dashboard.go 13 KB

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