loader.go 12 KB

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