dashboard.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. //go:build linux
  2. package main
  3. import (
  4. "fmt"
  5. "math"
  6. "os"
  7. "os/exec"
  8. "os/user"
  9. "path/filepath"
  10. "strconv"
  11. "strings"
  12. "syscall"
  13. "unicode"
  14. "github.com/AlecAivazis/survey/v2"
  15. "github.com/pbnjay/memory"
  16. log "github.com/sirupsen/logrus"
  17. "github.com/spf13/cobra"
  18. "github.com/crowdsecurity/crowdsec/pkg/metabase"
  19. "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
  20. )
  21. var (
  22. metabaseUser = "crowdsec@crowdsec.net"
  23. metabasePassword string
  24. metabaseDbPath string
  25. metabaseConfigPath string
  26. metabaseConfigFolder = "metabase/"
  27. metabaseConfigFile = "metabase.yaml"
  28. metabaseImage = "metabase/metabase:v0.46.6.1"
  29. /**/
  30. metabaseListenAddress = "127.0.0.1"
  31. metabaseListenPort = "3000"
  32. metabaseContainerID = "crowdsec-metabase"
  33. crowdsecGroup = "crowdsec"
  34. forceYes bool
  35. // information needed to set up a random password on user's behalf
  36. )
  37. func NewDashboardCmd() *cobra.Command {
  38. /* ---- UPDATE COMMAND */
  39. var cmdDashboard = &cobra.Command{
  40. Use: "dashboard [command]",
  41. Short: "Manage your metabase dashboard container [requires local API]",
  42. Long: `Install/Start/Stop/Remove a metabase container exposing dashboard and metrics.
  43. Note: This command requires database direct access, so is intended to be run on Local API/master.
  44. `,
  45. Args: cobra.ExactArgs(1),
  46. DisableAutoGenTag: true,
  47. Example: `
  48. cscli dashboard setup
  49. cscli dashboard start
  50. cscli dashboard stop
  51. cscli dashboard remove
  52. `,
  53. PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
  54. if err := require.LAPI(csConfig); err != nil {
  55. return err
  56. }
  57. if err := metabase.TestAvailability(); err != nil {
  58. return err
  59. }
  60. metabaseConfigFolderPath := filepath.Join(csConfig.ConfigPaths.ConfigDir, metabaseConfigFolder)
  61. metabaseConfigPath = filepath.Join(metabaseConfigFolderPath, metabaseConfigFile)
  62. if err := os.MkdirAll(metabaseConfigFolderPath, os.ModePerm); err != nil {
  63. return err
  64. }
  65. if err := require.DB(csConfig); err != nil {
  66. return err
  67. }
  68. /*
  69. Old container name was "/crowdsec-metabase" but podman doesn't
  70. allow '/' in container name. We do this check to not break
  71. existing dashboard setup.
  72. */
  73. if !metabase.IsContainerExist(metabaseContainerID) {
  74. oldContainerID := fmt.Sprintf("/%s", metabaseContainerID)
  75. if metabase.IsContainerExist(oldContainerID) {
  76. metabaseContainerID = oldContainerID
  77. }
  78. }
  79. return nil
  80. },
  81. }
  82. cmdDashboard.AddCommand(NewDashboardSetupCmd())
  83. cmdDashboard.AddCommand(NewDashboardStartCmd())
  84. cmdDashboard.AddCommand(NewDashboardStopCmd())
  85. cmdDashboard.AddCommand(NewDashboardShowPasswordCmd())
  86. cmdDashboard.AddCommand(NewDashboardRemoveCmd())
  87. return cmdDashboard
  88. }
  89. func NewDashboardSetupCmd() *cobra.Command {
  90. var force bool
  91. var cmdDashSetup = &cobra.Command{
  92. Use: "setup",
  93. Short: "Setup a metabase container.",
  94. Long: `Perform a metabase docker setup, download standard dashboards, create a fresh user and start the container`,
  95. Args: cobra.ExactArgs(0),
  96. DisableAutoGenTag: true,
  97. Example: `
  98. cscli dashboard setup
  99. cscli dashboard setup --listen 0.0.0.0
  100. cscli dashboard setup -l 0.0.0.0 -p 443 --password <password>
  101. `,
  102. RunE: func(cmd *cobra.Command, args []string) error {
  103. if metabaseDbPath == "" {
  104. metabaseDbPath = csConfig.ConfigPaths.DataDir
  105. }
  106. if metabasePassword == "" {
  107. isValid := passwordIsValid(metabasePassword)
  108. for !isValid {
  109. metabasePassword = generatePassword(16)
  110. isValid = passwordIsValid(metabasePassword)
  111. }
  112. }
  113. if err := checkSystemMemory(&forceYes); err != nil {
  114. return err
  115. }
  116. warnIfNotLoopback(metabaseListenAddress)
  117. if err := disclaimer(&forceYes); err != nil {
  118. return err
  119. }
  120. dockerGroup, err := checkGroups(&forceYes)
  121. if err != nil {
  122. return err
  123. }
  124. if err = chownDatabase(dockerGroup.Gid); err != nil {
  125. return err
  126. }
  127. mb, err := metabase.SetupMetabase(csConfig.API.Server.DbConfig, metabaseListenAddress, metabaseListenPort, metabaseUser, metabasePassword, metabaseDbPath, dockerGroup.Gid, metabaseContainerID, metabaseImage)
  128. if err != nil {
  129. return err
  130. }
  131. if err := mb.DumpConfig(metabaseConfigPath); err != nil {
  132. return err
  133. }
  134. log.Infof("Metabase is ready")
  135. fmt.Println()
  136. fmt.Printf("\tURL : '%s'\n", mb.Config.ListenURL)
  137. fmt.Printf("\tusername : '%s'\n", mb.Config.Username)
  138. fmt.Printf("\tpassword : '%s'\n", mb.Config.Password)
  139. return nil
  140. },
  141. }
  142. cmdDashSetup.Flags().BoolVarP(&force, "force", "f", false, "Force setup : override existing files")
  143. cmdDashSetup.Flags().StringVarP(&metabaseDbPath, "dir", "d", "", "Shared directory with metabase container")
  144. cmdDashSetup.Flags().StringVarP(&metabaseListenAddress, "listen", "l", metabaseListenAddress, "Listen address of container")
  145. cmdDashSetup.Flags().StringVar(&metabaseImage, "metabase-image", metabaseImage, "Metabase image to use")
  146. cmdDashSetup.Flags().StringVarP(&metabaseListenPort, "port", "p", metabaseListenPort, "Listen port of container")
  147. cmdDashSetup.Flags().BoolVarP(&forceYes, "yes", "y", false, "force yes")
  148. //cmdDashSetup.Flags().StringVarP(&metabaseUser, "user", "u", "crowdsec@crowdsec.net", "metabase user")
  149. cmdDashSetup.Flags().StringVar(&metabasePassword, "password", "", "metabase password")
  150. return cmdDashSetup
  151. }
  152. func NewDashboardStartCmd() *cobra.Command {
  153. var cmdDashStart = &cobra.Command{
  154. Use: "start",
  155. Short: "Start the metabase container.",
  156. Long: `Stats the metabase container using docker.`,
  157. Args: cobra.ExactArgs(0),
  158. DisableAutoGenTag: true,
  159. RunE: func(cmd *cobra.Command, args []string) error {
  160. mb, err := metabase.NewMetabase(metabaseConfigPath, metabaseContainerID)
  161. if err != nil {
  162. return err
  163. }
  164. warnIfNotLoopback(mb.Config.ListenAddr)
  165. if err := disclaimer(&forceYes); err != nil {
  166. return err
  167. }
  168. if err := mb.Container.Start(); err != nil {
  169. return fmt.Errorf("failed to start metabase container : %s", err)
  170. }
  171. log.Infof("Started metabase")
  172. log.Infof("url : http://%s:%s", mb.Config.ListenAddr, mb.Config.ListenPort)
  173. return nil
  174. },
  175. }
  176. cmdDashStart.Flags().BoolVarP(&forceYes, "yes", "y", false, "force yes")
  177. return cmdDashStart
  178. }
  179. func NewDashboardStopCmd() *cobra.Command {
  180. var cmdDashStop = &cobra.Command{
  181. Use: "stop",
  182. Short: "Stops the metabase container.",
  183. Long: `Stops the metabase container using docker.`,
  184. Args: cobra.ExactArgs(0),
  185. DisableAutoGenTag: true,
  186. RunE: func(cmd *cobra.Command, args []string) error {
  187. if err := metabase.StopContainer(metabaseContainerID); err != nil {
  188. return fmt.Errorf("unable to stop container '%s': %s", metabaseContainerID, err)
  189. }
  190. return nil
  191. },
  192. }
  193. return cmdDashStop
  194. }
  195. func NewDashboardShowPasswordCmd() *cobra.Command {
  196. var cmdDashShowPassword = &cobra.Command{Use: "show-password",
  197. Short: "displays password of metabase.",
  198. Args: cobra.ExactArgs(0),
  199. DisableAutoGenTag: true,
  200. RunE: func(cmd *cobra.Command, args []string) error {
  201. m := metabase.Metabase{}
  202. if err := m.LoadConfig(metabaseConfigPath); err != nil {
  203. return err
  204. }
  205. log.Printf("'%s'", m.Config.Password)
  206. return nil
  207. },
  208. }
  209. return cmdDashShowPassword
  210. }
  211. func NewDashboardRemoveCmd() *cobra.Command {
  212. var force bool
  213. var cmdDashRemove = &cobra.Command{
  214. Use: "remove",
  215. Short: "removes the metabase container.",
  216. Long: `removes the metabase container using docker.`,
  217. Args: cobra.ExactArgs(0),
  218. DisableAutoGenTag: true,
  219. Example: `
  220. cscli dashboard remove
  221. cscli dashboard remove --force
  222. `,
  223. RunE: func(cmd *cobra.Command, args []string) error {
  224. if !forceYes {
  225. var answer bool
  226. prompt := &survey.Confirm{
  227. Message: "Do you really want to remove crowdsec dashboard? (all your changes will be lost)",
  228. Default: true,
  229. }
  230. if err := survey.AskOne(prompt, &answer); err != nil {
  231. return fmt.Errorf("unable to ask to force: %s", err)
  232. }
  233. if !answer {
  234. return fmt.Errorf("user stated no to continue")
  235. }
  236. }
  237. if metabase.IsContainerExist(metabaseContainerID) {
  238. log.Debugf("Stopping container %s", metabaseContainerID)
  239. if err := metabase.StopContainer(metabaseContainerID); err != nil {
  240. log.Warningf("unable to stop container '%s': %s", metabaseContainerID, err)
  241. }
  242. dockerGroup, err := user.LookupGroup(crowdsecGroup)
  243. if err == nil { // if group exist, remove it
  244. groupDelCmd, err := exec.LookPath("groupdel")
  245. if err != nil {
  246. return fmt.Errorf("unable to find 'groupdel' command, can't continue")
  247. }
  248. groupDel := &exec.Cmd{Path: groupDelCmd, Args: []string{groupDelCmd, crowdsecGroup}}
  249. if err := groupDel.Run(); err != nil {
  250. log.Warnf("unable to delete group '%s': %s", dockerGroup, err)
  251. }
  252. }
  253. log.Debugf("Removing container %s", metabaseContainerID)
  254. if err := metabase.RemoveContainer(metabaseContainerID); err != nil {
  255. log.Warnf("unable to remove container '%s': %s", metabaseContainerID, err)
  256. }
  257. log.Infof("container %s stopped & removed", metabaseContainerID)
  258. }
  259. log.Debugf("Removing metabase db %s", csConfig.ConfigPaths.DataDir)
  260. if err := metabase.RemoveDatabase(csConfig.ConfigPaths.DataDir); err != nil {
  261. log.Warnf("failed to remove metabase internal db : %s", err)
  262. }
  263. if force {
  264. m := metabase.Metabase{}
  265. if err := m.LoadConfig(metabaseConfigPath); err != nil {
  266. return err
  267. }
  268. if err := metabase.RemoveImageContainer(m.Config.Image); err != nil {
  269. if !strings.Contains(err.Error(), "No such image") {
  270. return fmt.Errorf("removing docker image: %s", err)
  271. }
  272. }
  273. }
  274. return nil
  275. },
  276. }
  277. cmdDashRemove.Flags().BoolVarP(&force, "force", "f", false, "Remove also the metabase image")
  278. cmdDashRemove.Flags().BoolVarP(&forceYes, "yes", "y", false, "force yes")
  279. return cmdDashRemove
  280. }
  281. func passwordIsValid(password string) bool {
  282. hasDigit := false
  283. for _, j := range password {
  284. if unicode.IsDigit(j) {
  285. hasDigit = true
  286. break
  287. }
  288. }
  289. if !hasDigit || len(password) < 6 {
  290. return false
  291. }
  292. return true
  293. }
  294. func checkSystemMemory(forceYes *bool) error {
  295. totMem := memory.TotalMemory()
  296. if totMem >= uint64(math.Pow(2, 30)) {
  297. return nil
  298. }
  299. if !*forceYes {
  300. var answer bool
  301. prompt := &survey.Confirm{
  302. Message: "Metabase requires 1-2GB of RAM, your system is below this requirement continue ?",
  303. Default: true,
  304. }
  305. if err := survey.AskOne(prompt, &answer); err != nil {
  306. return fmt.Errorf("unable to ask about RAM check: %s", err)
  307. }
  308. if !answer {
  309. return fmt.Errorf("user stated no to continue")
  310. }
  311. return nil
  312. }
  313. log.Warn("Metabase requires 1-2GB of RAM, your system is below this requirement")
  314. return nil
  315. }
  316. func warnIfNotLoopback(addr string) {
  317. if addr == "127.0.0.1" || addr == "::1" {
  318. return
  319. }
  320. log.Warnf("You are potentially exposing your metabase port to the internet (addr: %s), please consider using a reverse proxy", addr)
  321. }
  322. func disclaimer(forceYes *bool) error {
  323. if !*forceYes {
  324. var answer bool
  325. prompt := &survey.Confirm{
  326. Message: "CrowdSec takes no responsibility for the security of your metabase instance. Do you accept these responsibilities ?",
  327. Default: true,
  328. }
  329. if err := survey.AskOne(prompt, &answer); err != nil {
  330. return fmt.Errorf("unable to ask to question: %s", err)
  331. }
  332. if !answer {
  333. return fmt.Errorf("user stated no to responsibilities")
  334. }
  335. return nil
  336. }
  337. log.Warn("CrowdSec takes no responsibility for the security of your metabase instance. You used force yes, so you accept this disclaimer")
  338. return nil
  339. }
  340. func checkGroups(forceYes *bool) (*user.Group, error) {
  341. dockerGroup, err := user.LookupGroup(crowdsecGroup)
  342. if err == nil {
  343. return dockerGroup, nil
  344. }
  345. if !*forceYes {
  346. var answer bool
  347. prompt := &survey.Confirm{
  348. Message: fmt.Sprintf("For metabase docker to be able to access SQLite file we need to add a new group called '%s' to the system, is it ok for you ?", crowdsecGroup),
  349. Default: true,
  350. }
  351. if err := survey.AskOne(prompt, &answer); err != nil {
  352. return dockerGroup, fmt.Errorf("unable to ask to question: %s", err)
  353. }
  354. if !answer {
  355. return dockerGroup, fmt.Errorf("unable to continue without creating '%s' group", crowdsecGroup)
  356. }
  357. }
  358. groupAddCmd, err := exec.LookPath("groupadd")
  359. if err != nil {
  360. return dockerGroup, fmt.Errorf("unable to find 'groupadd' command, can't continue")
  361. }
  362. groupAdd := &exec.Cmd{Path: groupAddCmd, Args: []string{groupAddCmd, crowdsecGroup}}
  363. if err := groupAdd.Run(); err != nil {
  364. return dockerGroup, fmt.Errorf("unable to add group '%s': %s", dockerGroup, err)
  365. }
  366. return user.LookupGroup(crowdsecGroup)
  367. }
  368. func chownDatabase(gid string) error {
  369. intID, err := strconv.Atoi(gid)
  370. if err != nil {
  371. return fmt.Errorf("unable to convert group ID to int: %s", err)
  372. }
  373. if stat, err := os.Stat(csConfig.DbConfig.DbPath); !os.IsNotExist(err) {
  374. info := stat.Sys()
  375. if err := os.Chown(csConfig.DbConfig.DbPath, int(info.(*syscall.Stat_t).Uid), intID); err != nil {
  376. return fmt.Errorf("unable to chown sqlite db file '%s': %s", csConfig.DbConfig.DbPath, err)
  377. }
  378. }
  379. if csConfig.DbConfig.Type == "sqlite" && csConfig.DbConfig.UseWal != nil && *csConfig.DbConfig.UseWal {
  380. for _, ext := range []string{"-wal", "-shm"} {
  381. file := csConfig.DbConfig.DbPath + ext
  382. if stat, err := os.Stat(file); !os.IsNotExist(err) {
  383. info := stat.Sys()
  384. if err := os.Chown(file, int(info.(*syscall.Stat_t).Uid), intID); err != nil {
  385. return fmt.Errorf("unable to chown sqlite db file '%s': %s", file, err)
  386. }
  387. }
  388. }
  389. }
  390. return nil
  391. }