sync.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. package cwhub
  2. import (
  3. "crypto/sha256"
  4. "fmt"
  5. "io"
  6. "os"
  7. "path/filepath"
  8. "sort"
  9. "strings"
  10. log "github.com/sirupsen/logrus"
  11. )
  12. func isYAMLFileName(path string) bool {
  13. return strings.HasSuffix(path, ".yaml") || strings.HasSuffix(path, ".yml")
  14. }
  15. func handleSymlink(path string) (string, error) {
  16. hubpath, err := os.Readlink(path)
  17. if err != nil {
  18. return "", fmt.Errorf("unable to read symlink of %s", path)
  19. }
  20. // the symlink target doesn't exist, user might have removed ~/.hub/hub/...yaml without deleting /etc/crowdsec/....yaml
  21. _, err = os.Lstat(hubpath)
  22. if os.IsNotExist(err) {
  23. log.Infof("%s is a symlink to %s that doesn't exist, deleting symlink", path, hubpath)
  24. // remove the symlink
  25. if err = os.Remove(path); err != nil {
  26. return "", fmt.Errorf("failed to unlink %s: %w", path, err)
  27. }
  28. // XXX: is this correct?
  29. return "", nil
  30. }
  31. return hubpath, nil
  32. }
  33. func getSHA256(filepath string) (string, error) {
  34. f, err := os.Open(filepath)
  35. if err != nil {
  36. return "", fmt.Errorf("unable to open '%s': %w", filepath, err)
  37. }
  38. defer f.Close()
  39. h := sha256.New()
  40. if _, err := io.Copy(h, f); err != nil {
  41. return "", fmt.Errorf("unable to calculate sha256 of '%s': %w", filepath, err)
  42. }
  43. return fmt.Sprintf("%x", h.Sum(nil)), nil
  44. }
  45. type itemFileInfo struct {
  46. fname string
  47. stage string
  48. ftype string
  49. fauthor string
  50. }
  51. func (h *Hub) getItemInfo(path string) (itemFileInfo, bool, error) {
  52. ret := itemFileInfo{}
  53. inhub := false
  54. hubDir := h.cfg.HubDir
  55. installDir := h.cfg.InstallDir
  56. subs := strings.Split(path, string(os.PathSeparator))
  57. log.Tracef("path:%s, hubdir:%s, installdir:%s", path, hubDir, installDir)
  58. log.Tracef("subs:%v", subs)
  59. // we're in hub (~/.hub/hub/)
  60. if strings.HasPrefix(path, hubDir) {
  61. log.Tracef("in hub dir")
  62. inhub = true
  63. //.../hub/parsers/s00-raw/crowdsec/skip-pretag.yaml
  64. //.../hub/scenarios/crowdsec/ssh_bf.yaml
  65. //.../hub/profiles/crowdsec/linux.yaml
  66. if len(subs) < 4 {
  67. return itemFileInfo{}, false, fmt.Errorf("path is too short : %s (%d)", path, len(subs))
  68. }
  69. ret.fname = subs[len(subs)-1]
  70. ret.fauthor = subs[len(subs)-2]
  71. ret.stage = subs[len(subs)-3]
  72. ret.ftype = subs[len(subs)-4]
  73. } else if strings.HasPrefix(path, installDir) { // we're in install /etc/crowdsec/<type>/...
  74. log.Tracef("in install dir")
  75. if len(subs) < 3 {
  76. return itemFileInfo{}, false, fmt.Errorf("path is too short: %s (%d)", path, len(subs))
  77. }
  78. ///.../config/parser/stage/file.yaml
  79. ///.../config/postoverflow/stage/file.yaml
  80. ///.../config/scenarios/scenar.yaml
  81. ///.../config/collections/linux.yaml //file is empty
  82. ret.fname = subs[len(subs)-1]
  83. ret.stage = subs[len(subs)-2]
  84. ret.ftype = subs[len(subs)-3]
  85. ret.fauthor = ""
  86. } else {
  87. return itemFileInfo{}, false, fmt.Errorf("file '%s' is not from hub '%s' nor from the configuration directory '%s'", path, hubDir, installDir)
  88. }
  89. log.Tracef("stage:%s ftype:%s", ret.stage, ret.ftype)
  90. // log.Infof("%s -> name:%s stage:%s", path, fname, stage)
  91. if ret.stage == SCENARIOS {
  92. ret.ftype = SCENARIOS
  93. ret.stage = ""
  94. } else if ret.stage == COLLECTIONS {
  95. ret.ftype = COLLECTIONS
  96. ret.stage = ""
  97. } else if ret.ftype != PARSERS && ret.ftype != POSTOVERFLOWS {
  98. // its a PARSER / POSTOVERFLOW with a stage
  99. return itemFileInfo{}, inhub, fmt.Errorf("unknown configuration type for file '%s'", path)
  100. }
  101. log.Tracef("CORRECTED [%s] by [%s] in stage [%s] of type [%s]", ret.fname, ret.fauthor, ret.stage, ret.ftype)
  102. return ret, inhub, nil
  103. }
  104. func (h *Hub) itemVisit(path string, f os.DirEntry, err error) error {
  105. var (
  106. local bool
  107. hubpath string
  108. )
  109. if err != nil {
  110. log.Debugf("while syncing hub dir: %s", err)
  111. // there is a path error, we ignore the file
  112. return nil
  113. }
  114. path, err = filepath.Abs(path)
  115. if err != nil {
  116. return err
  117. }
  118. // we only care about files
  119. if f == nil || f.IsDir() {
  120. return nil
  121. }
  122. if !isYAMLFileName(f.Name()) {
  123. return nil
  124. }
  125. info, inhub, err := h.getItemInfo(path)
  126. if err != nil {
  127. return err
  128. }
  129. /*
  130. we can encounter 'collections' in the form of a symlink :
  131. /etc/crowdsec/.../collections/linux.yaml -> ~/.hub/hub/collections/.../linux.yaml
  132. when the collection is installed, both files are created
  133. */
  134. // non symlinks are local user files or hub files
  135. if f.Type()&os.ModeSymlink == 0 {
  136. local = true
  137. log.Tracef("%s isn't a symlink", path)
  138. } else {
  139. hubpath, err = handleSymlink(path)
  140. if err != nil {
  141. return err
  142. }
  143. log.Tracef("%s points to %s", path, hubpath)
  144. if hubpath == "" {
  145. // XXX: is this correct?
  146. return nil
  147. }
  148. }
  149. // if it's not a symlink and not in hub, it's a local file, don't bother
  150. if local && !inhub {
  151. log.Tracef("%s is a local file, skip", path)
  152. h.skippedLocal++
  153. // log.Infof("local scenario, skip.")
  154. _, fileName := filepath.Split(path)
  155. h.Items[info.ftype][info.fname] = Item{
  156. Name: info.fname,
  157. Stage: info.stage,
  158. Installed: true,
  159. Type: info.ftype,
  160. Local: true,
  161. LocalPath: path,
  162. UpToDate: true,
  163. FileName: fileName,
  164. }
  165. return nil
  166. }
  167. // try to find which configuration item it is
  168. log.Tracef("check [%s] of %s", info.fname, info.ftype)
  169. match := false
  170. for name, item := range h.Items[info.ftype] {
  171. log.Tracef("check [%s] vs [%s] : %s", info.fname, item.RemotePath, info.ftype+"/"+info.stage+"/"+info.fname+".yaml")
  172. if info.fname != item.FileName {
  173. log.Tracef("%s != %s (filename)", info.fname, item.FileName)
  174. continue
  175. }
  176. // wrong stage
  177. if item.Stage != info.stage {
  178. continue
  179. }
  180. // if we are walking hub dir, just mark present files as downloaded
  181. if inhub {
  182. // wrong author
  183. if info.fauthor != item.Author {
  184. continue
  185. }
  186. // not the item we're looking for
  187. if !item.validPath(info.fauthor, info.fname) {
  188. continue
  189. }
  190. if path == h.cfg.HubDir+"/"+item.RemotePath {
  191. log.Tracef("marking %s as downloaded", item.Name)
  192. item.Downloaded = true
  193. }
  194. } else if !hasPathSuffix(hubpath, item.RemotePath) {
  195. // wrong file
  196. // <type>/<stage>/<author>/<name>.yaml
  197. continue
  198. }
  199. sha, err := getSHA256(path)
  200. if err != nil {
  201. log.Fatalf("Failed to get sha of %s : %v", path, err)
  202. }
  203. // let's reverse sort the versions to deal with hash collisions (#154)
  204. versions := make([]string, 0, len(item.Versions))
  205. for k := range item.Versions {
  206. versions = append(versions, k)
  207. }
  208. sort.Sort(sort.Reverse(sort.StringSlice(versions)))
  209. for _, version := range versions {
  210. val := item.Versions[version]
  211. if sha != val.Digest {
  212. // log.Infof("matching filenames, wrong hash %s != %s -- %s", sha, val.Digest, spew.Sdump(v))
  213. continue
  214. }
  215. // we got an exact match, update struct
  216. item.Downloaded = true
  217. item.LocalHash = sha
  218. if !inhub {
  219. log.Tracef("found exact match for %s, version is %s, latest is %s", item.Name, version, item.Version)
  220. item.LocalPath = path
  221. item.LocalVersion = version
  222. item.Tainted = false
  223. // if we're walking the hub, present file doesn't means installed file
  224. item.Installed = true
  225. }
  226. if version == item.Version {
  227. log.Tracef("%s is up-to-date", item.Name)
  228. item.UpToDate = true
  229. }
  230. match = true
  231. break
  232. }
  233. if !match {
  234. log.Tracef("got tainted match for %s: %s", item.Name, path)
  235. h.skippedTainted++
  236. // the file and the stage is right, but the hash is wrong, it has been tainted by user
  237. if !inhub {
  238. item.LocalPath = path
  239. item.Installed = true
  240. }
  241. item.UpToDate = false
  242. item.LocalVersion = "?"
  243. item.Tainted = true
  244. item.LocalHash = sha
  245. }
  246. h.Items[info.ftype][name] = item
  247. return nil
  248. }
  249. log.Infof("Ignoring file %s of type %s", path, info.ftype)
  250. return nil
  251. }
  252. func (h *Hub) CollectDepsCheck(v *Item) error {
  253. if v.Type != COLLECTIONS {
  254. return nil
  255. }
  256. if v.versionStatus() != 0 { // not up-to-date
  257. log.Debugf("%s dependencies not checked: not up-to-date", v.Name)
  258. return nil
  259. }
  260. // if it's a collection, ensure all the items are installed, or tag it as tainted
  261. log.Tracef("checking submembers of %s installed:%t", v.Name, v.Installed)
  262. for _, sub := range v.SubItems() {
  263. subItem, ok := h.Items[sub.Type][sub.Name]
  264. if !ok {
  265. return fmt.Errorf("referred %s %s in collection %s doesn't exist", sub.Type, sub.Name, v.Name)
  266. }
  267. log.Tracef("check %s installed:%t", subItem.Name, subItem.Installed)
  268. if !v.Installed {
  269. continue
  270. }
  271. if subItem.Type == COLLECTIONS {
  272. log.Tracef("collec, recurse.")
  273. if err := h.CollectDepsCheck(&subItem); err != nil {
  274. if subItem.Tainted {
  275. v.Tainted = true
  276. }
  277. return fmt.Errorf("sub collection %s is broken: %w", subItem.Name, err)
  278. }
  279. h.Items[sub.Type][sub.Name] = subItem
  280. }
  281. // propagate the state of sub-items to set
  282. if subItem.Tainted {
  283. v.Tainted = true
  284. return fmt.Errorf("tainted %s %s, tainted", sub.Type, sub.Name)
  285. }
  286. if !subItem.Installed && v.Installed {
  287. v.Tainted = true
  288. return fmt.Errorf("missing %s %s, tainted", sub.Type, sub.Name)
  289. }
  290. if !subItem.UpToDate {
  291. v.UpToDate = false
  292. return fmt.Errorf("outdated %s %s", sub.Type, sub.Name)
  293. }
  294. skip := false
  295. for idx := range subItem.BelongsToCollections {
  296. if subItem.BelongsToCollections[idx] == v.Name {
  297. skip = true
  298. }
  299. }
  300. if !skip {
  301. subItem.BelongsToCollections = append(subItem.BelongsToCollections, v.Name)
  302. }
  303. h.Items[sub.Type][sub.Name] = subItem
  304. log.Tracef("checking for %s - tainted:%t uptodate:%t", sub.Name, v.Tainted, v.UpToDate)
  305. }
  306. return nil
  307. }
  308. func (h *Hub) SyncDir(dir string) ([]string, error) {
  309. warnings := []string{}
  310. // For each, scan PARSERS, POSTOVERFLOWS, SCENARIOS and COLLECTIONS last
  311. for _, scan := range ItemTypes {
  312. cpath, err := filepath.Abs(fmt.Sprintf("%s/%s", dir, scan))
  313. if err != nil {
  314. log.Errorf("failed %s : %s", cpath, err)
  315. }
  316. err = filepath.WalkDir(cpath, h.itemVisit)
  317. if err != nil {
  318. return warnings, err
  319. }
  320. }
  321. for name, item := range h.Items[COLLECTIONS] {
  322. if !item.Installed {
  323. continue
  324. }
  325. vs := item.versionStatus()
  326. switch vs {
  327. case 0: // latest
  328. if err := h.CollectDepsCheck(&item); err != nil {
  329. warnings = append(warnings, fmt.Sprintf("dependency of %s: %s", item.Name, err))
  330. h.Items[COLLECTIONS][name] = item
  331. }
  332. case 1: // not up-to-date
  333. warnings = append(warnings, fmt.Sprintf("update for collection %s available (currently:%s, latest:%s)", item.Name, item.LocalVersion, item.Version))
  334. default: // version is higher than the highest available from hub?
  335. warnings = append(warnings, fmt.Sprintf("collection %s is in the future (currently:%s, latest:%s)", item.Name, item.LocalVersion, item.Version))
  336. }
  337. log.Debugf("installed (%s) - status:%d | installed:%s | latest : %s | full : %+v", item.Name, vs, item.LocalVersion, item.Version, item.Versions)
  338. }
  339. return warnings, nil
  340. }
  341. // Updates the info from HubInit() with the local state
  342. func (h *Hub) LocalSync() ([]string, error) {
  343. h.skippedLocal = 0
  344. h.skippedTainted = 0
  345. warnings, err := h.SyncDir(h.cfg.InstallDir)
  346. if err != nil {
  347. return warnings, fmt.Errorf("failed to scan %s: %w", h.cfg.InstallDir, err)
  348. }
  349. _, err = h.SyncDir(h.cfg.HubDir)
  350. if err != nil {
  351. return warnings, fmt.Errorf("failed to scan %s: %w", h.cfg.HubDir, err)
  352. }
  353. return warnings, nil
  354. }