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 }