mirror of
https://github.com/crowdsecurity/crowdsec.git
synced 2025-05-10 20:05:55 +02:00
Merge eca309ec4c
into 505ad36dfd
This commit is contained in:
commit
62aa1ebdf4
8 changed files with 15 additions and 1223 deletions
|
@ -5,47 +5,8 @@ package main
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unicode"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/pbnjay/memory"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/crowdsecurity/go-cs-lib/version"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
|
||||
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/idgen"
|
||||
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/metabase"
|
||||
)
|
||||
|
||||
var (
|
||||
metabaseUser = "crowdsec@crowdsec.net"
|
||||
metabasePassword string
|
||||
metabaseDBPath string
|
||||
metabaseConfigPath string
|
||||
metabaseConfigFolder = "metabase/"
|
||||
metabaseConfigFile = "metabase.yaml"
|
||||
metabaseImage = "metabase/metabase:v0.46.6.1"
|
||||
/**/
|
||||
metabaseListenAddress = "127.0.0.1"
|
||||
metabaseListenPort = "3000"
|
||||
metabaseContainerID = "crowdsec-metabase"
|
||||
metabaseContainerEnvironmentVariables []string
|
||||
crowdsecGroup = "crowdsec"
|
||||
|
||||
forceYes bool
|
||||
|
||||
// information needed to set up a random password on user's behalf
|
||||
)
|
||||
|
||||
type cliDashboard struct {
|
||||
|
@ -58,436 +19,22 @@ func NewCLIDashboard(cfg configGetter) *cliDashboard {
|
|||
}
|
||||
}
|
||||
|
||||
var ErrDashboardDeprecated = errors.New("command 'dashboard' has been removed, please read https://docs.crowdsec.net/blog/cscli_dashboard_deprecation/")
|
||||
|
||||
func (cli *cliDashboard) NewCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "dashboard [command]",
|
||||
Short: "Manage your metabase dashboard container [requires local API]",
|
||||
Long: `Install/Start/Stop/Remove a metabase container exposing dashboard and metrics.
|
||||
Note: This command requires database direct access, so is intended to be run on Local API/master.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
Example: `
|
||||
cscli dashboard setup
|
||||
cscli dashboard start
|
||||
cscli dashboard stop
|
||||
cscli dashboard remove
|
||||
`,
|
||||
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
|
||||
if version.System == "docker" {
|
||||
return errors.New("cscli dashboard is not supported whilst running CrowdSec within a container please see: https://github.com/crowdsecurity/example-docker-compose/tree/main/basic")
|
||||
}
|
||||
|
||||
cfg := cli.cfg()
|
||||
if err := require.LAPI(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := metabase.TestAvailability(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
metabaseConfigFolderPath := filepath.Join(cfg.ConfigPaths.ConfigDir, metabaseConfigFolder)
|
||||
metabaseConfigPath = filepath.Join(metabaseConfigFolderPath, metabaseConfigFile)
|
||||
if err := os.MkdirAll(metabaseConfigFolderPath, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := require.DB(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
/*
|
||||
Old container name was "/crowdsec-metabase" but podman doesn't
|
||||
allow '/' in container name. We do this check to not break
|
||||
existing dashboard setup.
|
||||
*/
|
||||
if !metabase.IsContainerExist(metabaseContainerID) {
|
||||
oldContainerID := fmt.Sprintf("/%s", metabaseContainerID)
|
||||
if metabase.IsContainerExist(oldContainerID) {
|
||||
metabaseContainerID = oldContainerID
|
||||
}
|
||||
}
|
||||
|
||||
log.Warn("cscli dashboard will be deprecated in version 1.7.0, read more at https://docs.crowdsec.net/blog/cscli_dashboard_deprecation/")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.AddCommand(cli.newSetupCmd())
|
||||
cmd.AddCommand(cli.newStartCmd())
|
||||
cmd.AddCommand(cli.newStopCmd())
|
||||
cmd.AddCommand(cli.newShowPasswordCmd())
|
||||
cmd.AddCommand(cli.newRemoveCmd())
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliDashboard) newSetupCmd() *cobra.Command {
|
||||
var force bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "setup",
|
||||
Short: "Setup a metabase container.",
|
||||
Long: `Perform a metabase docker setup, download standard dashboards, create a fresh user and start the container`,
|
||||
Args: args.NoArgs,
|
||||
DisableAutoGenTag: true,
|
||||
Example: `
|
||||
cscli dashboard setup
|
||||
cscli dashboard setup --listen 0.0.0.0
|
||||
cscli dashboard setup -l 0.0.0.0 -p 443 --password <password>
|
||||
`,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
if metabaseDBPath == "" {
|
||||
metabaseDBPath = cli.cfg().ConfigPaths.DataDir
|
||||
}
|
||||
|
||||
if metabasePassword == "" {
|
||||
isValid := passwordIsValid(metabasePassword)
|
||||
for !isValid {
|
||||
var err error
|
||||
metabasePassword, err = idgen.GeneratePassword(16)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
isValid = passwordIsValid(metabasePassword)
|
||||
}
|
||||
}
|
||||
if err := checkSystemMemory(&forceYes); err != nil {
|
||||
return err
|
||||
}
|
||||
warnIfNotLoopback(metabaseListenAddress)
|
||||
if err := disclaimer(&forceYes); err != nil {
|
||||
return err
|
||||
}
|
||||
dockerGroup, err := checkGroups(&forceYes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = cli.chownDatabase(dockerGroup.Gid); err != nil {
|
||||
return err
|
||||
}
|
||||
mb, err := metabase.SetupMetabase(cli.cfg().API.Server.DbConfig, metabaseListenAddress,
|
||||
metabaseListenPort, metabaseUser, metabasePassword, metabaseDBPath, dockerGroup.Gid,
|
||||
metabaseContainerID, metabaseImage, metabaseContainerEnvironmentVariables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := mb.DumpConfig(metabaseConfigPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("Metabase is ready")
|
||||
fmt.Println()
|
||||
fmt.Printf("\tURL : '%s'\n", mb.Config.ListenURL)
|
||||
fmt.Printf("\tusername : '%s'\n", mb.Config.Username)
|
||||
fmt.Printf("\tpassword : '%s'\n", mb.Config.Password)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&force, "force", "f", false, "Force setup : override existing files")
|
||||
flags.StringVarP(&metabaseDBPath, "dir", "d", "", "Shared directory with metabase container")
|
||||
flags.StringVarP(&metabaseListenAddress, "listen", "l", metabaseListenAddress, "Listen address of container")
|
||||
flags.StringVar(&metabaseImage, "metabase-image", metabaseImage, "Metabase image to use")
|
||||
flags.StringVarP(&metabaseListenPort, "port", "p", metabaseListenPort, "Listen port of container")
|
||||
flags.BoolVarP(&forceYes, "yes", "y", false, "force yes")
|
||||
// flags.StringVarP(&metabaseUser, "user", "u", "crowdsec@crowdsec.net", "metabase user")
|
||||
flags.StringVar(&metabasePassword, "password", "", "metabase password")
|
||||
flags.StringSliceVarP(&metabaseContainerEnvironmentVariables, "env", "e", nil, "Additional environment variables to pass to the metabase container")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliDashboard) newStartCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "start",
|
||||
Short: "Start the metabase container.",
|
||||
Long: `Stats the metabase container using docker.`,
|
||||
Args: args.NoArgs,
|
||||
Use: "dashboard [command]",
|
||||
Hidden: true,
|
||||
Short: "Manage your metabase dashboard container [requires local API]",
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
mb, err := metabase.NewMetabase(metabaseConfigPath, metabaseContainerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
warnIfNotLoopback(mb.Config.ListenAddr)
|
||||
if err := disclaimer(&forceYes); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := mb.Container.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start metabase container : %s", err)
|
||||
}
|
||||
log.Infof("Started metabase")
|
||||
log.Infof("url : http://%s:%s", mb.Config.ListenAddr, mb.Config.ListenPort)
|
||||
|
||||
return nil
|
||||
return ErrDashboardDeprecated
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&forceYes, "yes", "y", false, "force yes")
|
||||
cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println(ErrDashboardDeprecated.Error())
|
||||
})
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliDashboard) newStopCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "stop",
|
||||
Short: "Stops the metabase container.",
|
||||
Long: `Stops the metabase container using docker.`,
|
||||
Args: args.NoArgs,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
if err := metabase.StopContainer(metabaseContainerID); err != nil {
|
||||
return fmt.Errorf("unable to stop container '%s': %s", metabaseContainerID, err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliDashboard) newShowPasswordCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "show-password",
|
||||
Short: "displays password of metabase.",
|
||||
Args: args.NoArgs,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
m := metabase.Metabase{}
|
||||
if err := m.LoadConfig(metabaseConfigPath); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("'%s'", m.Config.Password)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliDashboard) newRemoveCmd() *cobra.Command {
|
||||
var force bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "remove",
|
||||
Short: "removes the metabase container.",
|
||||
Long: `removes the metabase container using docker.`,
|
||||
Args: args.NoArgs,
|
||||
DisableAutoGenTag: true,
|
||||
Example: `
|
||||
cscli dashboard remove
|
||||
cscli dashboard remove --force
|
||||
`,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
if !forceYes {
|
||||
var answer bool
|
||||
prompt := &survey.Confirm{
|
||||
Message: "Do you really want to remove crowdsec dashboard? (all your changes will be lost)",
|
||||
Default: true,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &answer); err != nil {
|
||||
return fmt.Errorf("unable to ask to force: %s", err)
|
||||
}
|
||||
if !answer {
|
||||
return errors.New("user stated no to continue")
|
||||
}
|
||||
}
|
||||
if metabase.IsContainerExist(metabaseContainerID) {
|
||||
log.Debugf("Stopping container %s", metabaseContainerID)
|
||||
if err := metabase.StopContainer(metabaseContainerID); err != nil {
|
||||
log.Warningf("unable to stop container '%s': %s", metabaseContainerID, err)
|
||||
}
|
||||
dockerGroup, err := user.LookupGroup(crowdsecGroup)
|
||||
if err == nil { // if group exist, remove it
|
||||
groupDelCmd, err := exec.LookPath("groupdel")
|
||||
if err != nil {
|
||||
return errors.New("unable to find 'groupdel' command, can't continue")
|
||||
}
|
||||
|
||||
groupDel := &exec.Cmd{Path: groupDelCmd, Args: []string{groupDelCmd, crowdsecGroup}}
|
||||
if err := groupDel.Run(); err != nil {
|
||||
log.Warnf("unable to delete group '%s': %s", dockerGroup, err)
|
||||
}
|
||||
}
|
||||
log.Debugf("Removing container %s", metabaseContainerID)
|
||||
if err := metabase.RemoveContainer(metabaseContainerID); err != nil {
|
||||
log.Warnf("unable to remove container '%s': %s", metabaseContainerID, err)
|
||||
}
|
||||
log.Infof("container %s stopped & removed", metabaseContainerID)
|
||||
}
|
||||
log.Debugf("Removing metabase db %s", cli.cfg().ConfigPaths.DataDir)
|
||||
if err := metabase.RemoveDatabase(cli.cfg().ConfigPaths.DataDir); err != nil {
|
||||
log.Warnf("failed to remove metabase internal db : %s", err)
|
||||
}
|
||||
if force {
|
||||
m := metabase.Metabase{}
|
||||
if err := m.LoadConfig(metabaseConfigPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := metabase.RemoveImageContainer(m.Config.Image); err != nil {
|
||||
if !strings.Contains(err.Error(), "No such image") {
|
||||
return fmt.Errorf("removing docker image: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&force, "force", "f", false, "Remove also the metabase image")
|
||||
flags.BoolVarP(&forceYes, "yes", "y", false, "force yes")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func passwordIsValid(password string) bool {
|
||||
hasDigit := false
|
||||
|
||||
for _, j := range password {
|
||||
if unicode.IsDigit(j) {
|
||||
hasDigit = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasDigit || len(password) < 6 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func checkSystemMemory(forceYes *bool) error {
|
||||
totMem := memory.TotalMemory()
|
||||
if totMem >= uint64(math.Pow(2, 30)) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !*forceYes {
|
||||
var answer bool
|
||||
|
||||
prompt := &survey.Confirm{
|
||||
Message: "Metabase requires 1-2GB of RAM, your system is below this requirement continue ?",
|
||||
Default: true,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &answer); err != nil {
|
||||
return fmt.Errorf("unable to ask about RAM check: %s", err)
|
||||
}
|
||||
|
||||
if !answer {
|
||||
return errors.New("user stated no to continue")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Warn("Metabase requires 1-2GB of RAM, your system is below this requirement")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func warnIfNotLoopback(addr string) {
|
||||
if addr == "127.0.0.1" || addr == "::1" {
|
||||
return
|
||||
}
|
||||
|
||||
log.Warnf("You are potentially exposing your metabase port to the internet (addr: %s), please consider using a reverse proxy", addr)
|
||||
}
|
||||
|
||||
func disclaimer(forceYes *bool) error {
|
||||
if !*forceYes {
|
||||
var answer bool
|
||||
|
||||
prompt := &survey.Confirm{
|
||||
Message: "CrowdSec takes no responsibility for the security of your metabase instance. Do you accept these responsibilities ?",
|
||||
Default: true,
|
||||
}
|
||||
|
||||
if err := survey.AskOne(prompt, &answer); err != nil {
|
||||
return fmt.Errorf("unable to ask to question: %s", err)
|
||||
}
|
||||
|
||||
if !answer {
|
||||
return errors.New("user stated no to responsibilities")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Warn("CrowdSec takes no responsibility for the security of your metabase instance. You used force yes, so you accept this disclaimer")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkGroups(forceYes *bool) (*user.Group, error) {
|
||||
dockerGroup, err := user.LookupGroup(crowdsecGroup)
|
||||
if err == nil {
|
||||
return dockerGroup, nil
|
||||
}
|
||||
|
||||
if !*forceYes {
|
||||
var answer bool
|
||||
|
||||
prompt := &survey.Confirm{
|
||||
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),
|
||||
Default: true,
|
||||
}
|
||||
|
||||
if err := survey.AskOne(prompt, &answer); err != nil {
|
||||
return dockerGroup, fmt.Errorf("unable to ask to question: %s", err)
|
||||
}
|
||||
|
||||
if !answer {
|
||||
return dockerGroup, fmt.Errorf("unable to continue without creating '%s' group", crowdsecGroup)
|
||||
}
|
||||
}
|
||||
|
||||
groupAddCmd, err := exec.LookPath("groupadd")
|
||||
if err != nil {
|
||||
return dockerGroup, errors.New("unable to find 'groupadd' command, can't continue")
|
||||
}
|
||||
|
||||
groupAdd := &exec.Cmd{Path: groupAddCmd, Args: []string{groupAddCmd, crowdsecGroup}}
|
||||
if err := groupAdd.Run(); err != nil {
|
||||
return dockerGroup, fmt.Errorf("unable to add group '%s': %s", dockerGroup, err)
|
||||
}
|
||||
|
||||
return user.LookupGroup(crowdsecGroup)
|
||||
}
|
||||
|
||||
func (cli *cliDashboard) chownDatabase(gid string) error {
|
||||
cfg := cli.cfg()
|
||||
intID, err := strconv.Atoi(gid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to convert group ID to int: %s", err)
|
||||
}
|
||||
|
||||
if stat, err := os.Stat(cfg.DbConfig.DbPath); !os.IsNotExist(err) {
|
||||
info := stat.Sys()
|
||||
if err := os.Chown(cfg.DbConfig.DbPath, int(info.(*syscall.Stat_t).Uid), intID); err != nil {
|
||||
return fmt.Errorf("unable to chown sqlite db file '%s': %s", cfg.DbConfig.DbPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.DbConfig.Type == "sqlite" && cfg.DbConfig.UseWal != nil && *cfg.DbConfig.UseWal {
|
||||
for _, ext := range []string{"-wal", "-shm"} {
|
||||
file := cfg.DbConfig.DbPath + ext
|
||||
if stat, err := os.Stat(file); !os.IsNotExist(err) {
|
||||
info := stat.Sys()
|
||||
if err := os.Chown(file, int(info.(*syscall.Stat_t).Uid), intID); err != nil {
|
||||
return fmt.Errorf("unable to chown sqlite db file '%s': %s", file, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
4
go.mod
4
go.mod
|
@ -28,10 +28,9 @@ require (
|
|||
github.com/crowdsecurity/grokky v0.2.2
|
||||
github.com/crowdsecurity/machineid v1.0.2
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
|
||||
github.com/dghubble/sling v1.4.2
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/docker v27.3.1+incompatible
|
||||
github.com/docker/go-connections v0.5.0
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/expr-lang/expr v1.17.2
|
||||
github.com/fatih/color v1.18.0
|
||||
|
@ -76,7 +75,6 @@ require (
|
|||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/oschwald/geoip2-golang v1.9.0
|
||||
github.com/oschwald/maxminddb-golang v1.12.0
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.17.0
|
||||
github.com/prometheus/client_model v0.5.0
|
||||
|
|
4
go.sum
4
go.sum
|
@ -122,8 +122,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
|||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dghubble/sling v1.4.2 h1:vs1HIGBbSl2SEALyU+irpYFLZMfc49Fp+jYryFebQjM=
|
||||
github.com/dghubble/sling v1.4.2/go.mod h1:o0arCOz0HwfqYQJLrRtqunaWOn4X6jxE/6ORKRpVTD4=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
|
||||
|
@ -569,8 +567,6 @@ github.com/oschwald/geoip2-golang v1.9.0 h1:uvD3O6fXAXs+usU+UGExshpdP13GAqp4GBrz
|
|||
github.com/oschwald/geoip2-golang v1.9.0/go.mod h1:BHK6TvDyATVQhKNbQBdrj9eAvuwOMi2zSFXizL3K81Y=
|
||||
github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs=
|
||||
github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
|
||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
|
||||
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
|
||||
|
|
|
@ -1,85 +0,0 @@
|
|||
package metabase
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/dghubble/sling"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/pkg/apiclient/useragent"
|
||||
)
|
||||
|
||||
type MBClient struct {
|
||||
CTX *sling.Sling
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
const (
|
||||
sessionEndpoint = "login"
|
||||
scanEndpoint = "scan"
|
||||
resetPasswordEndpoint = "reset_password"
|
||||
userEndpoint = "user"
|
||||
databaseEndpoint = "database"
|
||||
)
|
||||
|
||||
var (
|
||||
routes = map[string]string{
|
||||
sessionEndpoint: "api/session",
|
||||
scanEndpoint: "api/database/2/rescan_values",
|
||||
resetPasswordEndpoint: "api/user/1/password",
|
||||
userEndpoint: "api/user/1",
|
||||
databaseEndpoint: "api/database/2",
|
||||
}
|
||||
)
|
||||
|
||||
func NewMBClient(url string) (*MBClient, error) {
|
||||
httpClient := &http.Client{Timeout: 20 * time.Second}
|
||||
return &MBClient{
|
||||
CTX: sling.New().Client(httpClient).Base(url).Set("User-Agent", useragent.Default()),
|
||||
Client: httpClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *MBClient) Do(method string, route string, body interface{}) (interface{}, interface{}, error) {
|
||||
var Success interface{}
|
||||
var Error interface{}
|
||||
var resp *http.Response
|
||||
var err error
|
||||
var data []byte
|
||||
if body != nil {
|
||||
data, _ = json.Marshal(body)
|
||||
}
|
||||
|
||||
switch method {
|
||||
case "POST":
|
||||
log.Debugf("POST /%s", route)
|
||||
log.Debugf("%s", string(data))
|
||||
resp, err = h.CTX.New().Post(route).BodyJSON(body).Receive(&Success, &Error)
|
||||
case "GET":
|
||||
log.Debugf("GET /%s", route)
|
||||
resp, err = h.CTX.New().Get(route).Receive(&Success, &Error)
|
||||
case "PUT":
|
||||
log.Debugf("PUT /%s", route)
|
||||
log.Debugf("%s", string(data))
|
||||
resp, err = h.CTX.New().Put(route).BodyJSON(body).Receive(&Success, &Error)
|
||||
case "DELETE":
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("unsupported method '%s'", method)
|
||||
}
|
||||
if Error != nil {
|
||||
return Success, Error, fmt.Errorf("http error: %v", Error)
|
||||
}
|
||||
|
||||
if resp != nil && resp.StatusCode != 200 && resp.StatusCode != 202 {
|
||||
return Success, Error, fmt.Errorf("bad status code '%d': (success: %+v) | (error: %+v)", resp.StatusCode, Success, Error)
|
||||
}
|
||||
return Success, Error, err
|
||||
}
|
||||
|
||||
// Set headers as key:value
|
||||
func (h *MBClient) Set(key string, value string) {
|
||||
h.CTX = h.CTX.Set(key, value)
|
||||
}
|
|
@ -1,180 +0,0 @@
|
|||
package metabase
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
typesImage "github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/go-connections/nat"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/crowdsecurity/go-cs-lib/ptr"
|
||||
)
|
||||
|
||||
type Container struct {
|
||||
ListenAddr string
|
||||
ListenPort string
|
||||
SharedFolder string
|
||||
Image string
|
||||
Name string
|
||||
ID string
|
||||
CLI *client.Client
|
||||
MBDBUri string
|
||||
DockerGroupID string
|
||||
EnvironmentVariables []string
|
||||
}
|
||||
|
||||
func NewContainer(listenAddr string, listenPort string, sharedFolder string, containerName string, image string, mbDBURI string, dockerGroupID string, environmentVariables []string) (*Container, error) {
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create docker client : %s", err)
|
||||
}
|
||||
return &Container{
|
||||
ListenAddr: listenAddr,
|
||||
ListenPort: listenPort,
|
||||
SharedFolder: sharedFolder,
|
||||
Image: image,
|
||||
Name: containerName,
|
||||
CLI: cli,
|
||||
MBDBUri: mbDBURI,
|
||||
DockerGroupID: dockerGroupID,
|
||||
EnvironmentVariables: environmentVariables,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Container) Create() error {
|
||||
ctx := context.Background()
|
||||
log.Printf("Pulling docker image %s", c.Image)
|
||||
reader, err := c.CLI.ImagePull(ctx, c.Image, typesImage.PullOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to pull docker image : %s", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
scanner := bufio.NewScanner(reader)
|
||||
for scanner.Scan() {
|
||||
fmt.Print(".")
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return fmt.Errorf("failed to read imagepull reader: %s", err)
|
||||
}
|
||||
fmt.Print("\n")
|
||||
|
||||
hostConfig := &container.HostConfig{
|
||||
PortBindings: nat.PortMap{
|
||||
"3000/tcp": []nat.PortBinding{
|
||||
{
|
||||
HostIP: c.ListenAddr,
|
||||
HostPort: c.ListenPort,
|
||||
},
|
||||
},
|
||||
},
|
||||
Mounts: []mount.Mount{
|
||||
{
|
||||
Type: mount.TypeBind,
|
||||
Source: c.SharedFolder,
|
||||
Target: containerSharedFolder,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
env := c.EnvironmentVariables
|
||||
|
||||
env = append(env, fmt.Sprintf("MB_DB_FILE=%s/metabase.db", containerSharedFolder))
|
||||
if c.MBDBUri != "" {
|
||||
env = append(env, c.MBDBUri)
|
||||
}
|
||||
|
||||
env = append(env, fmt.Sprintf("MGID=%s", c.DockerGroupID))
|
||||
dockerConfig := &container.Config{
|
||||
Image: c.Image,
|
||||
Tty: true,
|
||||
Env: env,
|
||||
}
|
||||
|
||||
log.Infof("creating container '%s'", c.Name)
|
||||
resp, err := c.CLI.ContainerCreate(ctx, dockerConfig, hostConfig, nil, nil, c.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create container : %s", err)
|
||||
}
|
||||
c.ID = resp.ID
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Container) Start() error {
|
||||
ctx := context.Background()
|
||||
if err := c.CLI.ContainerStart(ctx, c.Name, container.StartOptions{}); err != nil {
|
||||
return fmt.Errorf("failed while starting %s : %s", c.ID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func StartContainer(name string) error {
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create docker client : %s", err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
if err := cli.ContainerStart(ctx, name, container.StartOptions{}); err != nil {
|
||||
return fmt.Errorf("failed while starting %s : %s", name, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func StopContainer(name string) error {
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create docker client : %s", err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
to := container.StopOptions{Timeout: ptr.Of(20)}
|
||||
if err := cli.ContainerStop(ctx, name, to); err != nil {
|
||||
return fmt.Errorf("failed while stopping %s : %s", name, err)
|
||||
}
|
||||
log.Printf("container stopped successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
func RemoveContainer(name string) error {
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create docker client : %s", err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
log.Printf("Removing docker metabase %s", name)
|
||||
if err := cli.ContainerRemove(ctx, name, container.RemoveOptions{}); err != nil {
|
||||
return fmt.Errorf("failed to remove container %s : %s", name, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func RemoveImageContainer(image string) error {
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create docker client : %s", err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
log.Printf("Removing docker image '%s'", image)
|
||||
if _, err := cli.ImageRemove(ctx, image, typesImage.RemoveOptions{}); err != nil {
|
||||
return fmt.Errorf("failed to remove image container %s : %s", image, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func IsContainerExist(name string) bool {
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create docker client : %s", err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
if _, err := cli.ContainerInspect(ctx, name); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
package metabase
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
DBUrl string
|
||||
Model *Model
|
||||
Config *csconfig.DatabaseCfg
|
||||
Client *MBClient
|
||||
Details *Details
|
||||
// in case mysql host is 127.0.0.1 the ip address of mysql/pgsql host will be the docker gateway since metabase run in a container
|
||||
}
|
||||
|
||||
type Details struct {
|
||||
Db string `json:"db"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Dbname string `json:"dbname"`
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
Ssl bool `json:"ssl"`
|
||||
AdditionalOptions interface{} `json:"additional-options"`
|
||||
TunnelEnabled bool `json:"tunnel-enabled"`
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
Engine string `json:"engine"`
|
||||
Name string `json:"name"`
|
||||
Details *Details `json:"details"`
|
||||
AutoRunQueries bool `json:"auto_run_queries"`
|
||||
IsFullSync bool `json:"is_full_sync"`
|
||||
IsOnDemand bool `json:"is_on_demand"`
|
||||
Schedules map[string]interface{} `json:"schedules"`
|
||||
}
|
||||
|
||||
func NewDatabase(config *csconfig.DatabaseCfg, client *MBClient, remoteDBAddr string) (*Database, error) {
|
||||
var details *Details
|
||||
|
||||
database := Database{}
|
||||
|
||||
switch config.Type {
|
||||
case "mysql":
|
||||
return nil, fmt.Errorf("database '%s' is not supported yet", config.Type)
|
||||
case "sqlite":
|
||||
database.DBUrl = metabaseSQLiteDBURL
|
||||
localFolder := filepath.Dir(config.DbPath)
|
||||
// replace /var/lib/crowdsec/data/ with /metabase-data/
|
||||
dbPath := strings.Replace(config.DbPath, localFolder, containerSharedFolder, 1)
|
||||
details = &Details{
|
||||
Db: dbPath,
|
||||
}
|
||||
case "postgresql", "postgres", "pgsql":
|
||||
return nil, fmt.Errorf("database '%s' is not supported yet", config.Type)
|
||||
default:
|
||||
return nil, fmt.Errorf("database '%s' not supported", config.Type)
|
||||
}
|
||||
database.Details = details
|
||||
database.Client = client
|
||||
database.Config = config
|
||||
|
||||
return &database, nil
|
||||
}
|
||||
|
||||
func (d *Database) Update() error {
|
||||
success, errormsg, err := d.Client.Do("GET", routes[databaseEndpoint], nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if errormsg != nil {
|
||||
return fmt.Errorf("update sqlite db http error: %+v", errormsg)
|
||||
}
|
||||
|
||||
data, err := json.Marshal(success)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update sqlite db response (marshal): %w", err)
|
||||
}
|
||||
|
||||
model := Model{}
|
||||
|
||||
if err := json.Unmarshal(data, &model); err != nil {
|
||||
return fmt.Errorf("update sqlite db response (unmarshal): %w", err)
|
||||
}
|
||||
model.Details = d.Details
|
||||
_, errormsg, err = d.Client.Do("PUT", routes[databaseEndpoint], model)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if errormsg != nil {
|
||||
return fmt.Errorf("update sqlite db http error: %+v", errormsg)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,388 +0,0 @@
|
|||
package metabase
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
|
||||
)
|
||||
|
||||
type Metabase struct {
|
||||
Config *Config
|
||||
Client *MBClient
|
||||
Container *Container
|
||||
Database *Database
|
||||
InternalDBURL string
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Database *csconfig.DatabaseCfg `yaml:"database"`
|
||||
ListenAddr string `yaml:"listen_addr"`
|
||||
ListenPort string `yaml:"listen_port"`
|
||||
ListenURL string `yaml:"listen_url"`
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
DBPath string `yaml:"metabase_db_path"`
|
||||
DockerGroupID string `yaml:"-"`
|
||||
Image string `yaml:"image"`
|
||||
EnvironmentVariables []string `yaml:"environment_variables"`
|
||||
}
|
||||
|
||||
var (
|
||||
metabaseDefaultUser = "crowdsec@crowdsec.net"
|
||||
metabaseDefaultPassword = "!!Cr0wdS3c_M3t4b4s3??"
|
||||
containerSharedFolder = "/metabase-data"
|
||||
metabaseSQLiteDBURL = "https://crowdsec-statics-assets.s3-eu-west-1.amazonaws.com/metabase_sqlite.zip"
|
||||
)
|
||||
|
||||
func TestAvailability() error {
|
||||
if runtime.GOARCH != "amd64" {
|
||||
return fmt.Errorf("cscli dashboard is only available on amd64, but you are running %s", runtime.GOARCH)
|
||||
}
|
||||
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create docker client : %s", err)
|
||||
}
|
||||
|
||||
_, err = cli.Ping(context.TODO())
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
func (m *Metabase) Init(containerName string, image string) error {
|
||||
var err error
|
||||
var DBConnectionURI string
|
||||
var remoteDBAddr string
|
||||
|
||||
switch m.Config.Database.Type {
|
||||
case "mysql":
|
||||
return errors.New("'mysql' is not supported yet for cscli dashboard")
|
||||
//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)
|
||||
case "sqlite":
|
||||
m.InternalDBURL = metabaseSQLiteDBURL
|
||||
case "postgresql", "postgres", "pgsql":
|
||||
return errors.New("'postgresql' is not supported yet by cscli dashboard")
|
||||
default:
|
||||
return fmt.Errorf("database '%s' not supported", m.Config.Database.Type)
|
||||
}
|
||||
|
||||
m.Client, err = NewMBClient(m.Config.ListenURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.Database, err = NewDatabase(m.Config.Database, m.Client, remoteDBAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.Container, err = NewContainer(m.Config.ListenAddr, m.Config.ListenPort, m.Config.DBPath, containerName, image, DBConnectionURI, m.Config.DockerGroupID, m.Config.EnvironmentVariables)
|
||||
if err != nil {
|
||||
return fmt.Errorf("container init: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
func NewMetabase(configPath string, containerName string) (*Metabase, error) {
|
||||
m := &Metabase{}
|
||||
if err := m.LoadConfig(configPath); err != nil {
|
||||
return m, err
|
||||
}
|
||||
if err := m.Init(containerName, m.Config.Image); err != nil {
|
||||
return m, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Metabase) LoadConfig(configPath string) error {
|
||||
yamlFile, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config := &Config{}
|
||||
|
||||
err = yaml.Unmarshal(yamlFile, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if config.Username == "" {
|
||||
return fmt.Errorf("'username' not found in configuration file '%s'", configPath)
|
||||
}
|
||||
|
||||
if config.Password == "" {
|
||||
return fmt.Errorf("'password' not found in configuration file '%s'", configPath)
|
||||
}
|
||||
|
||||
if config.ListenURL == "" {
|
||||
return fmt.Errorf("'listen_url' not found in configuration file '%s'", configPath)
|
||||
}
|
||||
/* Default image for backporting */
|
||||
if config.Image == "" {
|
||||
config.Image = "metabase/metabase:v0.41.5"
|
||||
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.")
|
||||
}
|
||||
m.Config = config
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetupMetabase(dbConfig *csconfig.DatabaseCfg, listenAddr string, listenPort string, username string, password string, mbDBPath string, dockerGroupID string, containerName string, image string, environmentVariables []string) (*Metabase, error) {
|
||||
metabase := &Metabase{
|
||||
Config: &Config{
|
||||
Database: dbConfig,
|
||||
ListenAddr: listenAddr,
|
||||
ListenPort: listenPort,
|
||||
Username: username,
|
||||
Password: password,
|
||||
ListenURL: fmt.Sprintf("http://%s:%s", listenAddr, listenPort),
|
||||
DBPath: mbDBPath,
|
||||
DockerGroupID: dockerGroupID,
|
||||
Image: image,
|
||||
EnvironmentVariables: environmentVariables,
|
||||
},
|
||||
}
|
||||
if err := metabase.Init(containerName, image); err != nil {
|
||||
return nil, fmt.Errorf("metabase setup init: %w", err)
|
||||
}
|
||||
|
||||
if err := metabase.DownloadDatabase(false); err != nil {
|
||||
return nil, fmt.Errorf("metabase db download: %w", err)
|
||||
}
|
||||
|
||||
if err := metabase.Container.Create(); err != nil {
|
||||
return nil, fmt.Errorf("container create: %w", err)
|
||||
}
|
||||
|
||||
if err := metabase.Container.Start(); err != nil {
|
||||
return nil, fmt.Errorf("container start: %w", err)
|
||||
}
|
||||
|
||||
log.Infof("waiting for metabase to be up (can take up to a minute)")
|
||||
if err := metabase.WaitAlive(); err != nil {
|
||||
return nil, fmt.Errorf("wait alive: %w", err)
|
||||
}
|
||||
|
||||
if err := metabase.Database.Update(); err != nil {
|
||||
return nil, fmt.Errorf("update database: %w", err)
|
||||
}
|
||||
|
||||
if err := metabase.Scan(); err != nil {
|
||||
return nil, fmt.Errorf("db scan: %w", err)
|
||||
}
|
||||
|
||||
if err := metabase.ResetCredentials(); err != nil {
|
||||
return nil, fmt.Errorf("reset creds: %w", err)
|
||||
}
|
||||
|
||||
return metabase, nil
|
||||
}
|
||||
|
||||
func (m *Metabase) WaitAlive() error {
|
||||
var err error
|
||||
for {
|
||||
err = m.Login(metabaseDefaultUser, metabaseDefaultPassword)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "password:did not match stored password") {
|
||||
log.Errorf("Password mismatch error, is your dashboard already setup ? Run 'cscli dashboard remove' to reset it.")
|
||||
return fmt.Errorf("password mismatch error: %w", err)
|
||||
}
|
||||
log.Debugf("%+v", err)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
|
||||
fmt.Printf(".")
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Metabase) Login(username string, password string) error {
|
||||
body := map[string]string{"username": username, "password": password}
|
||||
successmsg, errormsg, err := m.Client.Do("POST", routes[sessionEndpoint], body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if errormsg != nil {
|
||||
return fmt.Errorf("http login: %s", errormsg)
|
||||
}
|
||||
resp, ok := successmsg.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("login: bad response type: %+v", successmsg)
|
||||
}
|
||||
if _, ok = resp["id"]; !ok {
|
||||
return fmt.Errorf("login: can't update session id, no id in response: %v", successmsg)
|
||||
}
|
||||
id, ok := resp["id"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("login: bad id type: %+v", resp["id"])
|
||||
}
|
||||
m.Client.Set("Cookie", fmt.Sprintf("metabase.SESSION=%s", id))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Metabase) Scan() error {
|
||||
_, errormsg, err := m.Client.Do("POST", routes[scanEndpoint], nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if errormsg != nil {
|
||||
return fmt.Errorf("http scan: %s", errormsg)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Metabase) ResetPassword(current string, newPassword string) error {
|
||||
body := map[string]string{
|
||||
"id": "1",
|
||||
"password": newPassword,
|
||||
"old_password": current,
|
||||
}
|
||||
_, errormsg, err := m.Client.Do("PUT", routes[resetPasswordEndpoint], body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reset username: %w", err)
|
||||
}
|
||||
if errormsg != nil {
|
||||
return fmt.Errorf("http reset password: %s", errormsg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Metabase) ResetUsername(username string) error {
|
||||
body := struct {
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Email string `json:"email"`
|
||||
GroupIDs []int `json:"group_ids"`
|
||||
}{
|
||||
FirstName: "Crowdsec",
|
||||
LastName: "Crowdsec",
|
||||
Email: username,
|
||||
GroupIDs: []int{1, 2},
|
||||
}
|
||||
|
||||
_, errormsg, err := m.Client.Do("PUT", routes[userEndpoint], body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reset username: %w", err)
|
||||
}
|
||||
|
||||
if errormsg != nil {
|
||||
return fmt.Errorf("http reset username: %s", errormsg)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Metabase) ResetCredentials() error {
|
||||
if err := m.ResetPassword(metabaseDefaultPassword, m.Config.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
/*if err := m.ResetUsername(m.Config.Username); err != nil {
|
||||
return err
|
||||
}*/
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Metabase) DumpConfig(path string) error {
|
||||
data, err := yaml.Marshal(m.Config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, data, 0600)
|
||||
}
|
||||
|
||||
func (m *Metabase) DownloadDatabase(force bool) error {
|
||||
|
||||
metabaseDBSubpath := filepath.Join(m.Config.DBPath, "metabase.db")
|
||||
_, err := os.Stat(metabaseDBSubpath)
|
||||
if err == nil && !force {
|
||||
log.Printf("%s exists, skip.", metabaseDBSubpath)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(metabaseDBSubpath, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create %s : %s", metabaseDBSubpath, err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, m.InternalDBURL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build request to fetch metabase db : %s", err)
|
||||
}
|
||||
//This needs to be removed once we move the zip out of github
|
||||
//req.Header.Add("Accept", `application/vnd.github.v3.raw`)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed request to fetch metabase db : %s", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("got http %d while requesting metabase db %s, stop", resp.StatusCode, m.InternalDBURL)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed request read while fetching metabase db : %s", err)
|
||||
}
|
||||
log.Debugf("Got %d bytes archive", len(body))
|
||||
|
||||
if err := m.ExtractDatabase(bytes.NewReader(body)); err != nil {
|
||||
return fmt.Errorf("while extracting zip : %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Metabase) ExtractDatabase(buf *bytes.Reader) error {
|
||||
r, err := zip.NewReader(buf, int64(buf.Len()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, f := range r.File {
|
||||
if strings.Contains(f.Name, "..") {
|
||||
return fmt.Errorf("invalid path '%s' in archive", f.Name)
|
||||
}
|
||||
tfname := fmt.Sprintf("%s/%s", m.Config.DBPath, f.Name)
|
||||
log.Tracef("%s -> %d", f.Name, f.UncompressedSize64)
|
||||
if f.UncompressedSize64 == 0 {
|
||||
continue
|
||||
}
|
||||
tfd, err := os.OpenFile(tfname, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed opening target file '%s' : %s", tfname, err)
|
||||
}
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("while opening zip content %s : %s", f.Name, err)
|
||||
}
|
||||
written, err := io.Copy(tfd, rc)
|
||||
if errors.Is(err, io.EOF) {
|
||||
log.Printf("files finished ok")
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("while copying content to %s : %s", tfname, err)
|
||||
}
|
||||
log.Debugf("written %d bytes to %s", written, tfname)
|
||||
rc.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func RemoveDatabase(dataDir string) error {
|
||||
return os.RemoveAll(filepath.Join(dataDir, "metabase.db"))
|
||||
}
|
|
@ -293,7 +293,11 @@ teardown() {
|
|||
export CROWDSEC_FEATURE_CSCLI_SETUP="true"
|
||||
rune -0 cscli config feature-flags
|
||||
assert_line '✓ cscli_setup: Enable cscli setup command (service detection)'
|
||||
|
||||
# there are no retired features
|
||||
rune -0 cscli config feature-flags --retired
|
||||
}
|
||||
|
||||
@test "cscli dashboard" {
|
||||
rune -1 cscli dashboard xyz
|
||||
assert_stderr --partial "command 'dashboard' has been removed, please read https://docs.crowdsec.net/blog/cscli_dashboard_deprecation/"
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue