dashboard.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. package main
  2. import (
  3. "archive/zip"
  4. "bufio"
  5. "bytes"
  6. "context"
  7. "encoding/json"
  8. "fmt"
  9. "io"
  10. "io/ioutil"
  11. "net/http"
  12. "os"
  13. "path"
  14. "strings"
  15. "time"
  16. "github.com/crowdsecurity/crowdsec/pkg/cwversion"
  17. "github.com/dghubble/sling"
  18. "github.com/docker/docker/api/types"
  19. "github.com/docker/docker/api/types/container"
  20. "github.com/docker/docker/api/types/mount"
  21. "github.com/docker/docker/client"
  22. "github.com/docker/go-connections/nat"
  23. log "github.com/sirupsen/logrus"
  24. "github.com/spf13/cobra"
  25. )
  26. var (
  27. metabaseImage = "metabase/metabase"
  28. metabaseDbURI = "https://crowdsec-statics-assets.s3-eu-west-1.amazonaws.com/metabase.db.zip"
  29. metabaseDbPath = "/var/lib/crowdsec/data"
  30. /**/
  31. metabaseListenAddress = "127.0.0.1"
  32. metabaseListenPort = "3000"
  33. metabaseContainerID = "/crowdsec-metabase"
  34. /*informations needed to setup a random password on user's behalf*/
  35. metabaseURI = "http://localhost:3000/api/"
  36. metabaseURISession = "session"
  37. metabaseURIRescan = "database/2/rescan_values"
  38. metabaseURIUpdatepwd = "user/1/password"
  39. defaultPassword = "c6cmetabase"
  40. defaultEmail = "metabase@crowdsec.net"
  41. )
  42. func NewDashboardCmd() *cobra.Command {
  43. /* ---- UPDATE COMMAND */
  44. var cmdDashboard = &cobra.Command{
  45. Use: "dashboard",
  46. Short: "Start a dashboard (metabase) container.",
  47. Long: `Start a metabase container exposing dashboards and metrics.`,
  48. Args: cobra.ExactArgs(1),
  49. Example: `cscli dashboard setup
  50. cscli dashboard start
  51. cscli dashboard stop
  52. cscli dashboard setup --force`,
  53. }
  54. var force bool
  55. var cmdDashSetup = &cobra.Command{
  56. Use: "setup",
  57. Short: "Setup a metabase container.",
  58. Long: `Perform a metabase docker setup, download standard dashboards, create a fresh user and start the container`,
  59. Args: cobra.ExactArgs(0),
  60. Example: `cscli dashboard setup
  61. cscli dashboard setup --force
  62. cscli dashboard setup -l 0.0.0.0 -p 443
  63. `,
  64. Run: func(cmd *cobra.Command, args []string) {
  65. if err := downloadMetabaseDB(force); err != nil {
  66. log.Fatalf("Failed to download metabase DB : %s", err)
  67. }
  68. log.Infof("Downloaded metabase DB")
  69. if err := createMetabase(); err != nil {
  70. log.Fatalf("Failed to start metabase container : %s", err)
  71. }
  72. log.Infof("Started metabase")
  73. newpassword := generatePassword()
  74. if err := resetMetabasePassword(newpassword); err != nil {
  75. log.Fatalf("Failed to reset password : %s", err)
  76. }
  77. log.Infof("Setup finished")
  78. log.Infof("url : http://%s:%s", metabaseListenAddress, metabaseListenPort)
  79. log.Infof("username: %s", defaultEmail)
  80. log.Infof("password: %s", newpassword)
  81. },
  82. }
  83. cmdDashSetup.Flags().BoolVarP(&force, "force", "f", false, "Force setup : override existing files.")
  84. cmdDashSetup.Flags().StringVarP(&metabaseDbPath, "dir", "d", metabaseDbPath, "Shared directory with metabase container.")
  85. cmdDashSetup.Flags().StringVarP(&metabaseListenAddress, "listen", "l", metabaseListenAddress, "Listen address of container")
  86. cmdDashSetup.Flags().StringVarP(&metabaseListenPort, "port", "p", metabaseListenPort, "Listen port of container")
  87. cmdDashboard.AddCommand(cmdDashSetup)
  88. var cmdDashStart = &cobra.Command{
  89. Use: "start",
  90. Short: "Start the metabase container.",
  91. Long: `Stats the metabase container using docker.`,
  92. Args: cobra.ExactArgs(0),
  93. Run: func(cmd *cobra.Command, args []string) {
  94. if err := startMetabase(); err != nil {
  95. log.Fatalf("Failed to start metabase container : %s", err)
  96. }
  97. log.Infof("Started metabase")
  98. log.Infof("url : http://%s:%s", metabaseListenAddress, metabaseListenPort)
  99. },
  100. }
  101. cmdDashboard.AddCommand(cmdDashStart)
  102. var remove bool
  103. var cmdDashStop = &cobra.Command{
  104. Use: "stop",
  105. Short: "Stops the metabase container.",
  106. Long: `Stops the metabase container using docker.`,
  107. Args: cobra.ExactArgs(0),
  108. Run: func(cmd *cobra.Command, args []string) {
  109. if err := stopMetabase(remove); err != nil {
  110. log.Fatalf("Failed to stop metabase container : %s", err)
  111. }
  112. },
  113. }
  114. cmdDashStop.Flags().BoolVarP(&remove, "remove", "r", false, "remove (docker rm) container as well.")
  115. cmdDashboard.AddCommand(cmdDashStop)
  116. return cmdDashboard
  117. }
  118. func downloadMetabaseDB(force bool) error {
  119. metabaseDBSubpath := path.Join(metabaseDbPath, "metabase.db")
  120. _, err := os.Stat(metabaseDBSubpath)
  121. if err == nil && !force {
  122. log.Printf("%s exists, skip.", metabaseDBSubpath)
  123. return nil
  124. }
  125. if err := os.MkdirAll(metabaseDBSubpath, 0755); err != nil {
  126. return fmt.Errorf("failed to create %s : %s", metabaseDBSubpath, err)
  127. }
  128. req, err := http.NewRequest("GET", metabaseDbURI, nil)
  129. if err != nil {
  130. return fmt.Errorf("failed to build request to fetch metabase db : %s", err)
  131. }
  132. //This needs to be removed once we move the zip out of github
  133. req.Header.Add("Accept", `application/vnd.github.v3.raw`)
  134. resp, err := http.DefaultClient.Do(req)
  135. if err != nil {
  136. return fmt.Errorf("failed request to fetch metabase db : %s", err)
  137. }
  138. if resp.StatusCode != 200 {
  139. return fmt.Errorf("got http %d while requesting metabase db %s, stop", resp.StatusCode, metabaseDbURI)
  140. }
  141. defer resp.Body.Close()
  142. body, err := ioutil.ReadAll(resp.Body)
  143. if err != nil {
  144. return fmt.Errorf("failed request read while fetching metabase db : %s", err)
  145. }
  146. log.Printf("Got %d bytes archive", len(body))
  147. if err := extractMetabaseDB(bytes.NewReader(body)); err != nil {
  148. return fmt.Errorf("while extracting zip : %s", err)
  149. }
  150. return nil
  151. }
  152. func extractMetabaseDB(buf *bytes.Reader) error {
  153. r, err := zip.NewReader(buf, int64(buf.Len()))
  154. if err != nil {
  155. log.Fatal(err)
  156. }
  157. for _, f := range r.File {
  158. if strings.Contains(f.Name, "..") {
  159. return fmt.Errorf("invalid path '%s' in archive", f.Name)
  160. }
  161. tfname := fmt.Sprintf("%s/%s", metabaseDbPath, f.Name)
  162. log.Debugf("%s -> %d", f.Name, f.UncompressedSize64)
  163. if f.UncompressedSize64 == 0 {
  164. continue
  165. }
  166. tfd, err := os.OpenFile(tfname, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0644)
  167. if err != nil {
  168. return fmt.Errorf("failed opening target file '%s' : %s", tfname, err)
  169. }
  170. rc, err := f.Open()
  171. if err != nil {
  172. return fmt.Errorf("while opening zip content %s : %s", f.Name, err)
  173. }
  174. written, err := io.Copy(tfd, rc)
  175. if err == io.EOF {
  176. log.Printf("files finished ok")
  177. } else if err != nil {
  178. return fmt.Errorf("while copying content to %s : %s", tfname, err)
  179. }
  180. log.Infof("written %d bytes to %s", written, tfname)
  181. rc.Close()
  182. }
  183. return nil
  184. }
  185. func resetMetabasePassword(newpassword string) error {
  186. httpctx := sling.New().Base(metabaseURI).Set("User-Agent", fmt.Sprintf("CrowdWatch/%s", cwversion.VersionStr()))
  187. log.Printf("Waiting for metabase API to be up (can take up to a minute)")
  188. for {
  189. sessionreq, err := httpctx.New().Post(metabaseURISession).BodyJSON(map[string]string{"username": defaultEmail, "password": defaultPassword}).Request()
  190. if err != nil {
  191. return fmt.Errorf("api signin: HTTP request creation failed: %s", err)
  192. }
  193. httpClient := http.Client{Timeout: 20 * time.Second}
  194. resp, err := httpClient.Do(sessionreq)
  195. if err != nil {
  196. fmt.Printf(".")
  197. log.Debugf("While waiting for metabase to be up : %s", err)
  198. time.Sleep(1 * time.Second)
  199. continue
  200. }
  201. defer resp.Body.Close()
  202. fmt.Printf("\n")
  203. log.Printf("Metabase API is up")
  204. body, err := ioutil.ReadAll(resp.Body)
  205. if err != nil {
  206. return fmt.Errorf("metabase session unable to read API response body: '%s'", err)
  207. }
  208. if resp.StatusCode != 200 {
  209. return fmt.Errorf("metabase session http error (%d): %s", resp.StatusCode, string(body))
  210. }
  211. log.Printf("Successfully authenticated")
  212. jsonResp := make(map[string]string)
  213. err = json.Unmarshal(body, &jsonResp)
  214. if err != nil {
  215. return fmt.Errorf("failed to unmarshal metabase api response '%s': %s", string(body), err.Error())
  216. }
  217. log.Debugf("unmarshaled response : %v", jsonResp)
  218. httpctx = httpctx.Set("Cookie", fmt.Sprintf("metabase.SESSION=%s", jsonResp["id"]))
  219. break
  220. }
  221. /*rescan values*/
  222. sessionreq, err := httpctx.New().Post(metabaseURIRescan).Request()
  223. if err != nil {
  224. return fmt.Errorf("metabase rescan_values http error : %s", err)
  225. }
  226. httpClient := http.Client{Timeout: 20 * time.Second}
  227. resp, err := httpClient.Do(sessionreq)
  228. if err != nil {
  229. return fmt.Errorf("while trying to do rescan api call to metabase : %s", err)
  230. }
  231. defer resp.Body.Close()
  232. body, err := ioutil.ReadAll(resp.Body)
  233. if err != nil {
  234. return fmt.Errorf("while reading rescan api call response : %s", err)
  235. }
  236. if resp.StatusCode != 200 {
  237. return fmt.Errorf("got '%s' (http:%d) while trying to rescan metabase", string(body), resp.StatusCode)
  238. }
  239. /*update password*/
  240. sessionreq, err = httpctx.New().Put(metabaseURIUpdatepwd).BodyJSON(map[string]string{
  241. "id": "1",
  242. "password": newpassword,
  243. "old_password": defaultPassword}).Request()
  244. if err != nil {
  245. return fmt.Errorf("metabase password change http error : %s", err)
  246. }
  247. httpClient = http.Client{Timeout: 20 * time.Second}
  248. resp, err = httpClient.Do(sessionreq)
  249. if err != nil {
  250. return fmt.Errorf("while trying to reset metabase password : %s", err)
  251. }
  252. defer resp.Body.Close()
  253. body, err = ioutil.ReadAll(resp.Body)
  254. if err != nil {
  255. return fmt.Errorf("while reading from %s: '%s'", metabaseURIUpdatepwd, err)
  256. }
  257. if resp.StatusCode != 200 {
  258. log.Printf("Got %s (http:%d) while trying to reset password.", string(body), resp.StatusCode)
  259. log.Printf("Password has probably already been changed.")
  260. log.Printf("Use the dashboard install command to reset existing setup.")
  261. return fmt.Errorf("got http error %d on %s : %s", resp.StatusCode, metabaseURIUpdatepwd, string(body))
  262. }
  263. log.Printf("Changed password !")
  264. return nil
  265. }
  266. func startMetabase() error {
  267. ctx := context.Background()
  268. cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
  269. if err != nil {
  270. return fmt.Errorf("failed to create docker client : %s", err)
  271. }
  272. if err := cli.ContainerStart(ctx, metabaseContainerID, types.ContainerStartOptions{}); err != nil {
  273. return fmt.Errorf("failed while starting %s : %s", metabaseContainerID, err)
  274. }
  275. return nil
  276. }
  277. func stopMetabase(remove bool) error {
  278. log.Printf("Stop docker metabase %s", metabaseContainerID)
  279. ctx := context.Background()
  280. cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
  281. if err != nil {
  282. return fmt.Errorf("failed to create docker client : %s", err)
  283. }
  284. var to time.Duration = 20 * time.Second
  285. if err := cli.ContainerStop(ctx, metabaseContainerID, &to); err != nil {
  286. return fmt.Errorf("failed while stopping %s : %s", metabaseContainerID, err)
  287. }
  288. if remove {
  289. log.Printf("Removing docker metabase %s", metabaseContainerID)
  290. if err := cli.ContainerRemove(ctx, metabaseContainerID, types.ContainerRemoveOptions{}); err != nil {
  291. return fmt.Errorf("failed remove container %s : %s", metabaseContainerID, err)
  292. }
  293. }
  294. return nil
  295. }
  296. func createMetabase() error {
  297. ctx := context.Background()
  298. cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
  299. if err != nil {
  300. return fmt.Errorf("failed to start docker client : %s", err)
  301. }
  302. log.Printf("Pulling docker image %s", metabaseImage)
  303. reader, err := cli.ImagePull(ctx, metabaseImage, types.ImagePullOptions{})
  304. if err != nil {
  305. return fmt.Errorf("failed to pull docker image : %s", err)
  306. }
  307. defer reader.Close()
  308. scanner := bufio.NewScanner(reader)
  309. for scanner.Scan() {
  310. fmt.Print(".")
  311. }
  312. if err := scanner.Err(); err != nil {
  313. return fmt.Errorf("failed to read imagepull reader: %s", err)
  314. }
  315. fmt.Print("\n")
  316. hostConfig := &container.HostConfig{
  317. PortBindings: nat.PortMap{
  318. "3000/tcp": []nat.PortBinding{
  319. {
  320. HostIP: metabaseListenAddress,
  321. HostPort: metabaseListenPort,
  322. },
  323. },
  324. },
  325. Mounts: []mount.Mount{
  326. {
  327. Type: mount.TypeBind,
  328. Source: metabaseDbPath,
  329. Target: "/metabase-data",
  330. },
  331. },
  332. }
  333. dockerConfig := &container.Config{
  334. Image: metabaseImage,
  335. Tty: true,
  336. Env: []string{"MB_DB_FILE=/metabase-data/metabase.db"},
  337. }
  338. log.Printf("Creating container")
  339. resp, err := cli.ContainerCreate(ctx, dockerConfig, hostConfig, nil, metabaseContainerID)
  340. if err != nil {
  341. return fmt.Errorf("failed to create container : %s", err)
  342. }
  343. log.Printf("Starting container")
  344. if err := cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil {
  345. return fmt.Errorf("failed to start docker container : %s", err)
  346. }
  347. return nil
  348. }