github.com/crowdsecurity/crowdsec@v1.6.1/pkg/metabase/metabase.go (about)

     1  package metabase
     2  
     3  import (
     4  	"archive/zip"
     5  	"bytes"
     6  	"context"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"os"
    12  	"path/filepath"
    13  	"runtime"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/docker/docker/client"
    18  	log "github.com/sirupsen/logrus"
    19  	"gopkg.in/yaml.v2"
    20  
    21  	"github.com/crowdsecurity/crowdsec/pkg/csconfig"
    22  )
    23  
    24  type Metabase struct {
    25  	Config        *Config
    26  	Client        *MBClient
    27  	Container     *Container
    28  	Database      *Database
    29  	InternalDBURL string
    30  }
    31  
    32  type Config struct {
    33  	Database      *csconfig.DatabaseCfg `yaml:"database"`
    34  	ListenAddr    string                `yaml:"listen_addr"`
    35  	ListenPort    string                `yaml:"listen_port"`
    36  	ListenURL     string                `yaml:"listen_url"`
    37  	Username      string                `yaml:"username"`
    38  	Password      string                `yaml:"password"`
    39  	DBPath        string                `yaml:"metabase_db_path"`
    40  	DockerGroupID string                `yaml:"-"`
    41  	Image         string                `yaml:"image"`
    42  }
    43  
    44  var (
    45  	metabaseDefaultUser     = "crowdsec@crowdsec.net"
    46  	metabaseDefaultPassword = "!!Cr0wdS3c_M3t4b4s3??"
    47  	containerSharedFolder   = "/metabase-data"
    48  	metabaseSQLiteDBURL     = "https://crowdsec-statics-assets.s3-eu-west-1.amazonaws.com/metabase_sqlite.zip"
    49  )
    50  
    51  func TestAvailability() error {
    52  	if runtime.GOARCH != "amd64" {
    53  		return fmt.Errorf("cscli dashboard is only available on amd64, but you are running %s", runtime.GOARCH)
    54  	}
    55  
    56  	cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
    57  	if err != nil {
    58  		return fmt.Errorf("failed to create docker client : %s", err)
    59  	}
    60  
    61  	_, err = cli.Ping(context.TODO())
    62  	return err
    63  
    64  }
    65  
    66  func (m *Metabase) Init(containerName string, image string) error {
    67  	var err error
    68  	var DBConnectionURI string
    69  	var remoteDBAddr string
    70  
    71  	switch m.Config.Database.Type {
    72  	case "mysql":
    73  		return fmt.Errorf("'mysql' is not supported yet for cscli dashboard")
    74  		//DBConnectionURI = fmt.Sprintf("MB_DB_CONNECTION_URI=mysql://%s:%d/%s?user=%s&password=%s&allowPublicKeyRetrieval=true", remoteDBAddr, m.Config.Database.Port, m.Config.Database.DbName, m.Config.Database.User, m.Config.Database.Password)
    75  	case "sqlite":
    76  		m.InternalDBURL = metabaseSQLiteDBURL
    77  	case "postgresql", "postgres", "pgsql":
    78  		return fmt.Errorf("'postgresql' is not supported yet by cscli dashboard")
    79  	default:
    80  		return fmt.Errorf("database '%s' not supported", m.Config.Database.Type)
    81  	}
    82  
    83  	m.Client, err = NewMBClient(m.Config.ListenURL)
    84  	if err != nil {
    85  		return err
    86  	}
    87  	m.Database, err = NewDatabase(m.Config.Database, m.Client, remoteDBAddr)
    88  	if err != nil {
    89  		return err
    90  	}
    91  	m.Container, err = NewContainer(m.Config.ListenAddr, m.Config.ListenPort, m.Config.DBPath, containerName, image, DBConnectionURI, m.Config.DockerGroupID)
    92  	if err != nil {
    93  		return fmt.Errorf("container init: %w", err)
    94  	}
    95  
    96  	return nil
    97  }
    98  func NewMetabase(configPath string, containerName string) (*Metabase, error) {
    99  	m := &Metabase{}
   100  	if err := m.LoadConfig(configPath); err != nil {
   101  		return m, err
   102  	}
   103  	if err := m.Init(containerName, m.Config.Image); err != nil {
   104  		return m, err
   105  	}
   106  	return m, nil
   107  }
   108  
   109  func (m *Metabase) LoadConfig(configPath string) error {
   110  	yamlFile, err := os.ReadFile(configPath)
   111  	if err != nil {
   112  		return err
   113  	}
   114  
   115  	config := &Config{}
   116  
   117  	err = yaml.Unmarshal(yamlFile, config)
   118  	if err != nil {
   119  		return err
   120  	}
   121  	if config.Username == "" {
   122  		return fmt.Errorf("'username' not found in configuration file '%s'", configPath)
   123  	}
   124  
   125  	if config.Password == "" {
   126  		return fmt.Errorf("'password' not found in configuration file '%s'", configPath)
   127  	}
   128  
   129  	if config.ListenURL == "" {
   130  		return fmt.Errorf("'listen_url' not found in configuration file '%s'", configPath)
   131  	}
   132  	/* Default image for backporting */
   133  	if config.Image == "" {
   134  		config.Image = "metabase/metabase:v0.41.5"
   135  		log.Warn("Image not found in configuration file, you are using an old dashboard setup (v0.41.5), please remove your dashboard and re-create it to use the latest version.")
   136  	}
   137  	m.Config = config
   138  
   139  	return nil
   140  
   141  }
   142  
   143  func SetupMetabase(dbConfig *csconfig.DatabaseCfg, listenAddr string, listenPort string, username string, password string, mbDBPath string, dockerGroupID string, containerName string, image string) (*Metabase, error) {
   144  	metabase := &Metabase{
   145  		Config: &Config{
   146  			Database:      dbConfig,
   147  			ListenAddr:    listenAddr,
   148  			ListenPort:    listenPort,
   149  			Username:      username,
   150  			Password:      password,
   151  			ListenURL:     fmt.Sprintf("http://%s:%s", listenAddr, listenPort),
   152  			DBPath:        mbDBPath,
   153  			DockerGroupID: dockerGroupID,
   154  			Image:         image,
   155  		},
   156  	}
   157  	if err := metabase.Init(containerName, image); err != nil {
   158  		return nil, fmt.Errorf("metabase setup init: %w", err)
   159  	}
   160  
   161  	if err := metabase.DownloadDatabase(false); err != nil {
   162  		return nil, fmt.Errorf("metabase db download: %w", err)
   163  	}
   164  
   165  	if err := metabase.Container.Create(); err != nil {
   166  		return nil, fmt.Errorf("container create: %w", err)
   167  	}
   168  
   169  	if err := metabase.Container.Start(); err != nil {
   170  		return nil, fmt.Errorf("container start: %w", err)
   171  	}
   172  
   173  	log.Infof("waiting for metabase to be up (can take up to a minute)")
   174  	if err := metabase.WaitAlive(); err != nil {
   175  		return nil, fmt.Errorf("wait alive: %w", err)
   176  	}
   177  
   178  	if err := metabase.Database.Update(); err != nil {
   179  		return nil, fmt.Errorf("update database: %w", err)
   180  	}
   181  
   182  	if err := metabase.Scan(); err != nil {
   183  		return nil, fmt.Errorf("db scan: %w", err)
   184  	}
   185  
   186  	if err := metabase.ResetCredentials(); err != nil {
   187  		return nil, fmt.Errorf("reset creds: %w", err)
   188  	}
   189  
   190  	return metabase, nil
   191  }
   192  
   193  func (m *Metabase) WaitAlive() error {
   194  	var err error
   195  	for {
   196  		err = m.Login(metabaseDefaultUser, metabaseDefaultPassword)
   197  		if err != nil {
   198  			if strings.Contains(err.Error(), "password:did not match stored password") {
   199  				log.Errorf("Password mismatch error, is your dashboard already setup ? Run 'cscli dashboard remove' to reset it.")
   200  				return fmt.Errorf("password mismatch error: %w", err)
   201  			}
   202  			log.Debugf("%+v", err)
   203  		} else {
   204  			break
   205  		}
   206  
   207  		fmt.Printf(".")
   208  		time.Sleep(2 * time.Second)
   209  	}
   210  	fmt.Printf("\n")
   211  	return nil
   212  }
   213  
   214  func (m *Metabase) Login(username string, password string) error {
   215  	body := map[string]string{"username": username, "password": password}
   216  	successmsg, errormsg, err := m.Client.Do("POST", routes[sessionEndpoint], body)
   217  	if err != nil {
   218  		return err
   219  	}
   220  
   221  	if errormsg != nil {
   222  		return fmt.Errorf("http login: %s", errormsg)
   223  	}
   224  	resp, ok := successmsg.(map[string]interface{})
   225  	if !ok {
   226  		return fmt.Errorf("login: bad response type: %+v", successmsg)
   227  	}
   228  	if _, ok = resp["id"]; !ok {
   229  		return fmt.Errorf("login: can't update session id, no id in response: %v", successmsg)
   230  	}
   231  	id, ok := resp["id"].(string)
   232  	if !ok {
   233  		return fmt.Errorf("login: bad id type: %+v", resp["id"])
   234  	}
   235  	m.Client.Set("Cookie", fmt.Sprintf("metabase.SESSION=%s", id))
   236  	return nil
   237  }
   238  
   239  func (m *Metabase) Scan() error {
   240  	_, errormsg, err := m.Client.Do("POST", routes[scanEndpoint], nil)
   241  	if err != nil {
   242  		return err
   243  	}
   244  	if errormsg != nil {
   245  		return fmt.Errorf("http scan: %s", errormsg)
   246  	}
   247  
   248  	return nil
   249  }
   250  
   251  func (m *Metabase) ResetPassword(current string, newPassword string) error {
   252  	body := map[string]string{
   253  		"id":           "1",
   254  		"password":     newPassword,
   255  		"old_password": current,
   256  	}
   257  	_, errormsg, err := m.Client.Do("PUT", routes[resetPasswordEndpoint], body)
   258  	if err != nil {
   259  		return fmt.Errorf("reset username: %w", err)
   260  	}
   261  	if errormsg != nil {
   262  		return fmt.Errorf("http reset password: %s", errormsg)
   263  	}
   264  	return nil
   265  }
   266  
   267  func (m *Metabase) ResetUsername(username string) error {
   268  	body := struct {
   269  		FirstName string `json:"first_name"`
   270  		LastName  string `json:"last_name"`
   271  		Email     string `json:"email"`
   272  		GroupIDs  []int  `json:"group_ids"`
   273  	}{
   274  		FirstName: "Crowdsec",
   275  		LastName:  "Crowdsec",
   276  		Email:     username,
   277  		GroupIDs:  []int{1, 2},
   278  	}
   279  
   280  	_, errormsg, err := m.Client.Do("PUT", routes[userEndpoint], body)
   281  	if err != nil {
   282  		return fmt.Errorf("reset username: %w", err)
   283  	}
   284  
   285  	if errormsg != nil {
   286  		return fmt.Errorf("http reset username: %s", errormsg)
   287  	}
   288  
   289  	return nil
   290  }
   291  
   292  func (m *Metabase) ResetCredentials() error {
   293  	if err := m.ResetPassword(metabaseDefaultPassword, m.Config.Password); err != nil {
   294  		return err
   295  	}
   296  
   297  	/*if err := m.ResetUsername(m.Config.Username); err != nil {
   298  		return err
   299  	}*/
   300  
   301  	return nil
   302  }
   303  
   304  func (m *Metabase) DumpConfig(path string) error {
   305  	data, err := yaml.Marshal(m.Config)
   306  	if err != nil {
   307  		return err
   308  	}
   309  	return os.WriteFile(path, data, 0600)
   310  }
   311  
   312  func (m *Metabase) DownloadDatabase(force bool) error {
   313  
   314  	metabaseDBSubpath := filepath.Join(m.Config.DBPath, "metabase.db")
   315  	_, err := os.Stat(metabaseDBSubpath)
   316  	if err == nil && !force {
   317  		log.Printf("%s exists, skip.", metabaseDBSubpath)
   318  		return nil
   319  	}
   320  
   321  	if err := os.MkdirAll(metabaseDBSubpath, 0755); err != nil {
   322  		return fmt.Errorf("failed to create %s : %s", metabaseDBSubpath, err)
   323  	}
   324  
   325  	req, err := http.NewRequest(http.MethodGet, m.InternalDBURL, nil)
   326  	if err != nil {
   327  		return fmt.Errorf("failed to build request to fetch metabase db : %s", err)
   328  	}
   329  	//This needs to be removed once we move the zip out of github
   330  	//req.Header.Add("Accept", `application/vnd.github.v3.raw`)
   331  	resp, err := http.DefaultClient.Do(req)
   332  	if err != nil {
   333  		return fmt.Errorf("failed request to fetch metabase db : %s", err)
   334  	}
   335  	if resp.StatusCode != http.StatusOK {
   336  		return fmt.Errorf("got http %d while requesting metabase db %s, stop", resp.StatusCode, m.InternalDBURL)
   337  	}
   338  	defer resp.Body.Close()
   339  	body, err := io.ReadAll(resp.Body)
   340  	if err != nil {
   341  		return fmt.Errorf("failed request read while fetching metabase db : %s", err)
   342  	}
   343  	log.Debugf("Got %d bytes archive", len(body))
   344  
   345  	if err := m.ExtractDatabase(bytes.NewReader(body)); err != nil {
   346  		return fmt.Errorf("while extracting zip : %s", err)
   347  	}
   348  	return nil
   349  }
   350  
   351  func (m *Metabase) ExtractDatabase(buf *bytes.Reader) error {
   352  	r, err := zip.NewReader(buf, int64(buf.Len()))
   353  	if err != nil {
   354  		return err
   355  	}
   356  	for _, f := range r.File {
   357  		if strings.Contains(f.Name, "..") {
   358  			return fmt.Errorf("invalid path '%s' in archive", f.Name)
   359  		}
   360  		tfname := fmt.Sprintf("%s/%s", m.Config.DBPath, f.Name)
   361  		log.Tracef("%s -> %d", f.Name, f.UncompressedSize64)
   362  		if f.UncompressedSize64 == 0 {
   363  			continue
   364  		}
   365  		tfd, err := os.OpenFile(tfname, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0644)
   366  		if err != nil {
   367  			return fmt.Errorf("failed opening target file '%s' : %s", tfname, err)
   368  		}
   369  		rc, err := f.Open()
   370  		if err != nil {
   371  			return fmt.Errorf("while opening zip content %s : %s", f.Name, err)
   372  		}
   373  		written, err := io.Copy(tfd, rc)
   374  		if errors.Is(err, io.EOF) {
   375  			log.Printf("files finished ok")
   376  		} else if err != nil {
   377  			return fmt.Errorf("while copying content to %s : %s", tfname, err)
   378  		}
   379  		log.Debugf("written %d bytes to %s", written, tfname)
   380  		rc.Close()
   381  	}
   382  	return nil
   383  }
   384  
   385  func RemoveDatabase(dataDir string) error {
   386  	return os.RemoveAll(filepath.Join(dataDir, "metabase.db"))
   387  }