github.com/crowdsecurity/crowdsec@v1.6.1/pkg/acquisition/modules/docker/docker.go (about)

     1  package dockeracquisition
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"fmt"
     7  	"net/url"
     8  	"regexp"
     9  	"strconv"
    10  	"strings"
    11  	"time"
    12  
    13  	dockerTypes "github.com/docker/docker/api/types"
    14  	"github.com/docker/docker/client"
    15  	"github.com/prometheus/client_golang/prometheus"
    16  	log "github.com/sirupsen/logrus"
    17  	"gopkg.in/tomb.v2"
    18  	"gopkg.in/yaml.v2"
    19  
    20  	"github.com/crowdsecurity/dlog"
    21  
    22  	"github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration"
    23  	"github.com/crowdsecurity/crowdsec/pkg/types"
    24  )
    25  
    26  var linesRead = prometheus.NewCounterVec(
    27  	prometheus.CounterOpts{
    28  		Name: "cs_dockersource_hits_total",
    29  		Help: "Total lines that were read.",
    30  	},
    31  	[]string{"source"})
    32  
    33  type DockerConfiguration struct {
    34  	CheckInterval                     string   `yaml:"check_interval"`
    35  	FollowStdout                      bool     `yaml:"follow_stdout"`
    36  	FollowStdErr                      bool     `yaml:"follow_stderr"`
    37  	Until                             string   `yaml:"until"`
    38  	Since                             string   `yaml:"since"`
    39  	DockerHost                        string   `yaml:"docker_host"`
    40  	ContainerName                     []string `yaml:"container_name"`
    41  	ContainerID                       []string `yaml:"container_id"`
    42  	ContainerNameRegexp               []string `yaml:"container_name_regexp"`
    43  	ContainerIDRegexp                 []string `yaml:"container_id_regexp"`
    44  	ForceInotify                      bool     `yaml:"force_inotify"`
    45  	configuration.DataSourceCommonCfg `yaml:",inline"`
    46  }
    47  
    48  type DockerSource struct {
    49  	metricsLevel          int
    50  	Config                DockerConfiguration
    51  	runningContainerState map[string]*ContainerConfig
    52  	compiledContainerName []*regexp.Regexp
    53  	compiledContainerID   []*regexp.Regexp
    54  	CheckIntervalDuration time.Duration
    55  	logger                *log.Entry
    56  	Client                client.CommonAPIClient
    57  	t                     *tomb.Tomb
    58  	containerLogsOptions  *dockerTypes.ContainerLogsOptions
    59  }
    60  
    61  type ContainerConfig struct {
    62  	Name   string
    63  	ID     string
    64  	t      *tomb.Tomb
    65  	logger *log.Entry
    66  	Labels map[string]string
    67  	Tty    bool
    68  }
    69  
    70  func (d *DockerSource) GetUuid() string {
    71  	return d.Config.UniqueId
    72  }
    73  
    74  func (d *DockerSource) UnmarshalConfig(yamlConfig []byte) error {
    75  	d.Config = DockerConfiguration{
    76  		FollowStdout:  true, // default
    77  		FollowStdErr:  true, // default
    78  		CheckInterval: "1s", // default
    79  	}
    80  
    81  	err := yaml.UnmarshalStrict(yamlConfig, &d.Config)
    82  	if err != nil {
    83  		return fmt.Errorf("while parsing DockerAcquisition configuration: %w", err)
    84  	}
    85  
    86  	if d.logger != nil {
    87  		d.logger.Tracef("DockerAcquisition configuration: %+v", d.Config)
    88  	}
    89  
    90  	if len(d.Config.ContainerName) == 0 && len(d.Config.ContainerID) == 0 && len(d.Config.ContainerIDRegexp) == 0 && len(d.Config.ContainerNameRegexp) == 0 {
    91  		return fmt.Errorf("no containers names or containers ID configuration provided")
    92  	}
    93  
    94  	d.CheckIntervalDuration, err = time.ParseDuration(d.Config.CheckInterval)
    95  	if err != nil {
    96  		return fmt.Errorf("parsing 'check_interval' parameters: %s", d.CheckIntervalDuration)
    97  	}
    98  
    99  	if d.Config.Mode == "" {
   100  		d.Config.Mode = configuration.TAIL_MODE
   101  	}
   102  	if d.Config.Mode != configuration.CAT_MODE && d.Config.Mode != configuration.TAIL_MODE {
   103  		return fmt.Errorf("unsupported mode %s for docker datasource", d.Config.Mode)
   104  	}
   105  
   106  	for _, cont := range d.Config.ContainerNameRegexp {
   107  		d.compiledContainerName = append(d.compiledContainerName, regexp.MustCompile(cont))
   108  	}
   109  
   110  	for _, cont := range d.Config.ContainerIDRegexp {
   111  		d.compiledContainerID = append(d.compiledContainerID, regexp.MustCompile(cont))
   112  	}
   113  
   114  	if d.Config.Since == "" {
   115  		d.Config.Since = time.Now().UTC().Format(time.RFC3339)
   116  	}
   117  
   118  	d.containerLogsOptions = &dockerTypes.ContainerLogsOptions{
   119  		ShowStdout: d.Config.FollowStdout,
   120  		ShowStderr: d.Config.FollowStdErr,
   121  		Follow:     true,
   122  		Since:      d.Config.Since,
   123  	}
   124  
   125  	if d.Config.Until != "" {
   126  		d.containerLogsOptions.Until = d.Config.Until
   127  	}
   128  
   129  	return nil
   130  }
   131  
   132  func (d *DockerSource) Configure(yamlConfig []byte, logger *log.Entry, MetricsLevel int) error {
   133  	d.logger = logger
   134  	d.metricsLevel = MetricsLevel
   135  	err := d.UnmarshalConfig(yamlConfig)
   136  	if err != nil {
   137  		return err
   138  	}
   139  
   140  	d.runningContainerState = make(map[string]*ContainerConfig)
   141  
   142  	d.logger.Tracef("Actual DockerAcquisition configuration %+v", d.Config)
   143  
   144  	dockerClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
   145  	if err != nil {
   146  		return err
   147  	}
   148  
   149  	if d.Config.DockerHost != "" {
   150  		err = client.WithHost(d.Config.DockerHost)(dockerClient)
   151  		if err != nil {
   152  			return err
   153  		}
   154  	}
   155  	d.Client = dockerClient
   156  
   157  	_, err = d.Client.Info(context.Background())
   158  	if err != nil {
   159  		return fmt.Errorf("failed to configure docker datasource %s: %w", d.Config.DockerHost, err)
   160  	}
   161  
   162  	return nil
   163  }
   164  
   165  func (d *DockerSource) ConfigureByDSN(dsn string, labels map[string]string, logger *log.Entry, uuid string) error {
   166  	var err error
   167  
   168  	if !strings.HasPrefix(dsn, d.GetName()+"://") {
   169  		return fmt.Errorf("invalid DSN %s for docker source, must start with %s://", dsn, d.GetName())
   170  	}
   171  
   172  	d.Config = DockerConfiguration{
   173  		FollowStdout:  true,
   174  		FollowStdErr:  true,
   175  		CheckInterval: "1s",
   176  	}
   177  	d.Config.UniqueId = uuid
   178  	d.Config.ContainerName = make([]string, 0)
   179  	d.Config.ContainerID = make([]string, 0)
   180  	d.runningContainerState = make(map[string]*ContainerConfig)
   181  	d.Config.Mode = configuration.CAT_MODE
   182  	d.logger = logger
   183  	d.Config.Labels = labels
   184  
   185  	dockerClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
   186  	if err != nil {
   187  		return err
   188  	}
   189  
   190  	d.containerLogsOptions = &dockerTypes.ContainerLogsOptions{
   191  		ShowStdout: d.Config.FollowStdout,
   192  		ShowStderr: d.Config.FollowStdErr,
   193  		Follow:     false,
   194  	}
   195  	dsn = strings.TrimPrefix(dsn, d.GetName()+"://")
   196  	args := strings.Split(dsn, "?")
   197  
   198  	if len(args) == 0 {
   199  		return fmt.Errorf("invalid dsn: %s", dsn)
   200  	}
   201  
   202  	if len(args) == 1 && args[0] == "" {
   203  		return fmt.Errorf("empty %s DSN", d.GetName()+"://")
   204  	}
   205  	d.Config.ContainerName = append(d.Config.ContainerName, args[0])
   206  	// we add it as an ID also so user can provide docker name or docker ID
   207  	d.Config.ContainerID = append(d.Config.ContainerID, args[0])
   208  
   209  	// no parameters
   210  	if len(args) == 1 {
   211  		d.Client = dockerClient
   212  		return nil
   213  	}
   214  
   215  	parameters, err := url.ParseQuery(args[1])
   216  	if err != nil {
   217  		return fmt.Errorf("while parsing parameters %s: %w", dsn, err)
   218  	}
   219  
   220  	for k, v := range parameters {
   221  		switch k {
   222  		case "log_level":
   223  			if len(v) != 1 {
   224  				return fmt.Errorf("only one 'log_level' parameters is required, not many")
   225  			}
   226  			lvl, err := log.ParseLevel(v[0])
   227  			if err != nil {
   228  				return fmt.Errorf("unknown level %s: %w", v[0], err)
   229  			}
   230  			d.logger.Logger.SetLevel(lvl)
   231  		case "until":
   232  			if len(v) != 1 {
   233  				return fmt.Errorf("only one 'until' parameters is required, not many")
   234  			}
   235  			d.containerLogsOptions.Until = v[0]
   236  		case "since":
   237  			if len(v) != 1 {
   238  				return fmt.Errorf("only one 'since' parameters is required, not many")
   239  			}
   240  			d.containerLogsOptions.Since = v[0]
   241  		case "follow_stdout":
   242  			if len(v) != 1 {
   243  				return fmt.Errorf("only one 'follow_stdout' parameters is required, not many")
   244  			}
   245  			followStdout, err := strconv.ParseBool(v[0])
   246  			if err != nil {
   247  				return fmt.Errorf("parsing 'follow_stdout' parameters: %s", err)
   248  			}
   249  			d.Config.FollowStdout = followStdout
   250  			d.containerLogsOptions.ShowStdout = followStdout
   251  		case "follow_stderr":
   252  			if len(v) != 1 {
   253  				return fmt.Errorf("only one 'follow_stderr' parameters is required, not many")
   254  			}
   255  			followStdErr, err := strconv.ParseBool(v[0])
   256  			if err != nil {
   257  				return fmt.Errorf("parsing 'follow_stderr' parameters: %s", err)
   258  			}
   259  			d.Config.FollowStdErr = followStdErr
   260  			d.containerLogsOptions.ShowStderr = followStdErr
   261  		case "docker_host":
   262  			if len(v) != 1 {
   263  				return fmt.Errorf("only one 'docker_host' parameters is required, not many")
   264  			}
   265  			if err := client.WithHost(v[0])(dockerClient); err != nil {
   266  				return err
   267  			}
   268  		}
   269  	}
   270  	d.Client = dockerClient
   271  	return nil
   272  }
   273  
   274  func (d *DockerSource) GetMode() string {
   275  	return d.Config.Mode
   276  }
   277  
   278  // SupportedModes returns the supported modes by the acquisition module
   279  func (d *DockerSource) SupportedModes() []string {
   280  	return []string{configuration.TAIL_MODE, configuration.CAT_MODE}
   281  }
   282  
   283  // OneShotAcquisition reads a set of file and returns when done
   284  func (d *DockerSource) OneShotAcquisition(out chan types.Event, t *tomb.Tomb) error {
   285  	d.logger.Debug("In oneshot")
   286  	runningContainer, err := d.Client.ContainerList(context.Background(), dockerTypes.ContainerListOptions{})
   287  	if err != nil {
   288  		return err
   289  	}
   290  	foundOne := false
   291  	for _, container := range runningContainer {
   292  		if _, ok := d.runningContainerState[container.ID]; ok {
   293  			d.logger.Debugf("container with id %s is already being read from", container.ID)
   294  			continue
   295  		}
   296  		if containerConfig, ok := d.EvalContainer(container); ok {
   297  			d.logger.Infof("reading logs from container %s", containerConfig.Name)
   298  			d.logger.Debugf("logs options: %+v", *d.containerLogsOptions)
   299  			dockerReader, err := d.Client.ContainerLogs(context.Background(), containerConfig.ID, *d.containerLogsOptions)
   300  			if err != nil {
   301  				d.logger.Errorf("unable to read logs from container: %+v", err)
   302  				return err
   303  			}
   304  			// we use this library to normalize docker API logs (cf. https://ahmet.im/blog/docker-logs-api-binary-format-explained/)
   305  			foundOne = true
   306  			var scanner *bufio.Scanner
   307  			if containerConfig.Tty {
   308  				scanner = bufio.NewScanner(dockerReader)
   309  			} else {
   310  				reader := dlog.NewReader(dockerReader)
   311  				scanner = bufio.NewScanner(reader)
   312  			}
   313  			for scanner.Scan() {
   314  				select {
   315  				case <-t.Dying():
   316  					d.logger.Infof("Shutting down reader for container %s", containerConfig.Name)
   317  				default:
   318  					line := scanner.Text()
   319  					if line == "" {
   320  						continue
   321  					}
   322  					l := types.Line{}
   323  					l.Raw = line
   324  					l.Labels = d.Config.Labels
   325  					l.Time = time.Now().UTC()
   326  					l.Src = containerConfig.Name
   327  					l.Process = true
   328  					l.Module = d.GetName()
   329  					if d.metricsLevel != configuration.METRICS_NONE {
   330  						linesRead.With(prometheus.Labels{"source": containerConfig.Name}).Inc()
   331  					}
   332  					evt := types.Event{Line: l, Process: true, Type: types.LOG, ExpectMode: types.TIMEMACHINE}
   333  					out <- evt
   334  					d.logger.Debugf("Sent line to parsing: %+v", evt.Line.Raw)
   335  				}
   336  			}
   337  			err = scanner.Err()
   338  			if err != nil {
   339  				d.logger.Errorf("Got error from docker read: %s", err)
   340  			}
   341  			d.runningContainerState[container.ID] = containerConfig
   342  		}
   343  	}
   344  
   345  	t.Kill(nil)
   346  
   347  	if !foundOne {
   348  		return fmt.Errorf("no container found named: %s, can't run one shot acquisition", d.Config.ContainerName[0])
   349  	}
   350  
   351  	return nil
   352  }
   353  
   354  func (d *DockerSource) GetMetrics() []prometheus.Collector {
   355  	return []prometheus.Collector{linesRead}
   356  }
   357  
   358  func (d *DockerSource) GetAggregMetrics() []prometheus.Collector {
   359  	return []prometheus.Collector{linesRead}
   360  }
   361  
   362  func (d *DockerSource) GetName() string {
   363  	return "docker"
   364  }
   365  
   366  func (d *DockerSource) CanRun() error {
   367  	return nil
   368  }
   369  
   370  func (d *DockerSource) getContainerTTY(containerId string) bool {
   371  	containerDetails, err := d.Client.ContainerInspect(context.Background(), containerId)
   372  	if err != nil {
   373  		return false
   374  	}
   375  	return containerDetails.Config.Tty
   376  }
   377  
   378  func (d *DockerSource) EvalContainer(container dockerTypes.Container) (*ContainerConfig, bool) {
   379  	for _, containerID := range d.Config.ContainerID {
   380  		if containerID == container.ID {
   381  			return &ContainerConfig{ID: container.ID, Name: container.Names[0], Labels: d.Config.Labels, Tty: d.getContainerTTY(container.ID)}, true
   382  		}
   383  	}
   384  
   385  	for _, containerName := range d.Config.ContainerName {
   386  		for _, name := range container.Names {
   387  			if strings.HasPrefix(name, "/") && len(name) > 0 {
   388  				name = name[1:]
   389  			}
   390  			if name == containerName {
   391  				return &ContainerConfig{ID: container.ID, Name: name, Labels: d.Config.Labels, Tty: d.getContainerTTY(container.ID)}, true
   392  			}
   393  		}
   394  
   395  	}
   396  
   397  	for _, cont := range d.compiledContainerID {
   398  		if matched := cont.MatchString(container.ID); matched {
   399  			return &ContainerConfig{ID: container.ID, Name: container.Names[0], Labels: d.Config.Labels, Tty: d.getContainerTTY(container.ID)}, true
   400  		}
   401  	}
   402  
   403  	for _, cont := range d.compiledContainerName {
   404  		for _, name := range container.Names {
   405  			if matched := cont.MatchString(name); matched {
   406  				return &ContainerConfig{ID: container.ID, Name: name, Labels: d.Config.Labels, Tty: d.getContainerTTY(container.ID)}, true
   407  			}
   408  		}
   409  
   410  	}
   411  
   412  	return &ContainerConfig{}, false
   413  }
   414  
   415  func (d *DockerSource) WatchContainer(monitChan chan *ContainerConfig, deleteChan chan *ContainerConfig) error {
   416  	ticker := time.NewTicker(d.CheckIntervalDuration)
   417  	d.logger.Infof("Container watcher started, interval: %s", d.CheckIntervalDuration.String())
   418  	for {
   419  		select {
   420  		case <-d.t.Dying():
   421  			d.logger.Infof("stopping container watcher")
   422  			return nil
   423  		case <-ticker.C:
   424  			// to track for garbage collection
   425  			runningContainersID := make(map[string]bool)
   426  			runningContainer, err := d.Client.ContainerList(context.Background(), dockerTypes.ContainerListOptions{})
   427  			if err != nil {
   428  				if strings.Contains(strings.ToLower(err.Error()), "cannot connect to the docker daemon at") {
   429  					for idx, container := range d.runningContainerState {
   430  						if d.runningContainerState[idx].t.Alive() {
   431  							d.logger.Infof("killing tail for container %s", container.Name)
   432  							d.runningContainerState[idx].t.Kill(nil)
   433  							if err := d.runningContainerState[idx].t.Wait(); err != nil {
   434  								d.logger.Infof("error while waiting for death of %s : %s", container.Name, err)
   435  							}
   436  						}
   437  						delete(d.runningContainerState, idx)
   438  					}
   439  				} else {
   440  					log.Errorf("container list err: %s", err)
   441  				}
   442  				continue
   443  			}
   444  
   445  			for _, container := range runningContainer {
   446  				runningContainersID[container.ID] = true
   447  
   448  				// don't need to re eval an already monitored container
   449  				if _, ok := d.runningContainerState[container.ID]; ok {
   450  					continue
   451  				}
   452  				if containerConfig, ok := d.EvalContainer(container); ok {
   453  					monitChan <- containerConfig
   454  				}
   455  			}
   456  
   457  			for containerStateID, containerConfig := range d.runningContainerState {
   458  				if _, ok := runningContainersID[containerStateID]; !ok {
   459  					deleteChan <- containerConfig
   460  				}
   461  			}
   462  			d.logger.Tracef("Reading logs from %d containers", len(d.runningContainerState))
   463  
   464  			ticker.Reset(d.CheckIntervalDuration)
   465  		}
   466  	}
   467  }
   468  
   469  func (d *DockerSource) StreamingAcquisition(out chan types.Event, t *tomb.Tomb) error {
   470  	d.t = t
   471  	monitChan := make(chan *ContainerConfig)
   472  	deleteChan := make(chan *ContainerConfig)
   473  	d.logger.Infof("Starting docker acquisition")
   474  	t.Go(func() error {
   475  		return d.DockerManager(monitChan, deleteChan, out)
   476  	})
   477  
   478  	return d.WatchContainer(monitChan, deleteChan)
   479  }
   480  
   481  func (d *DockerSource) Dump() interface{} {
   482  	return d
   483  }
   484  
   485  func ReadTailScanner(scanner *bufio.Scanner, out chan string, t *tomb.Tomb) error {
   486  	for scanner.Scan() {
   487  		out <- scanner.Text()
   488  	}
   489  	return scanner.Err()
   490  }
   491  
   492  func (d *DockerSource) TailDocker(container *ContainerConfig, outChan chan types.Event, deleteChan chan *ContainerConfig) error {
   493  	container.logger.Infof("start tail for container %s", container.Name)
   494  	dockerReader, err := d.Client.ContainerLogs(context.Background(), container.ID, *d.containerLogsOptions)
   495  	if err != nil {
   496  		container.logger.Errorf("unable to read logs from container: %+v", err)
   497  		return err
   498  	}
   499  
   500  	var scanner *bufio.Scanner
   501  	// we use this library to normalize docker API logs (cf. https://ahmet.im/blog/docker-logs-api-binary-format-explained/)
   502  	if container.Tty {
   503  		scanner = bufio.NewScanner(dockerReader)
   504  	} else {
   505  		reader := dlog.NewReader(dockerReader)
   506  		scanner = bufio.NewScanner(reader)
   507  	}
   508  	readerChan := make(chan string)
   509  	readerTomb := &tomb.Tomb{}
   510  	readerTomb.Go(func() error {
   511  		return ReadTailScanner(scanner, readerChan, readerTomb)
   512  	})
   513  	for {
   514  		select {
   515  		case <-container.t.Dying():
   516  			readerTomb.Kill(nil)
   517  			container.logger.Infof("tail stopped for container %s", container.Name)
   518  			return nil
   519  		case line := <-readerChan:
   520  			if line == "" {
   521  				continue
   522  			}
   523  			l := types.Line{}
   524  			l.Raw = line
   525  			l.Labels = d.Config.Labels
   526  			l.Time = time.Now().UTC()
   527  			l.Src = container.Name
   528  			l.Process = true
   529  			l.Module = d.GetName()
   530  			var evt types.Event
   531  			if !d.Config.UseTimeMachine {
   532  				evt = types.Event{Line: l, Process: true, Type: types.LOG, ExpectMode: types.LIVE}
   533  			} else {
   534  				evt = types.Event{Line: l, Process: true, Type: types.LOG, ExpectMode: types.TIMEMACHINE}
   535  			}
   536  			linesRead.With(prometheus.Labels{"source": container.Name}).Inc()
   537  			outChan <- evt
   538  			d.logger.Debugf("Sent line to parsing: %+v", evt.Line.Raw)
   539  		case <-readerTomb.Dying():
   540  			//This case is to handle temporarily losing the connection to the docker socket
   541  			//The only known case currently is when using docker-socket-proxy (and maybe a docker daemon restart)
   542  			d.logger.Debugf("readerTomb dying for container %s, removing it from runningContainerState", container.Name)
   543  			deleteChan <- container
   544  			//Also reset the Since to avoid re-reading logs
   545  			d.Config.Since = time.Now().UTC().Format(time.RFC3339)
   546  			d.containerLogsOptions.Since = d.Config.Since
   547  			return nil
   548  		}
   549  	}
   550  }
   551  
   552  func (d *DockerSource) DockerManager(in chan *ContainerConfig, deleteChan chan *ContainerConfig, outChan chan types.Event) error {
   553  	d.logger.Info("DockerSource Manager started")
   554  	for {
   555  		select {
   556  		case newContainer := <-in:
   557  			if _, ok := d.runningContainerState[newContainer.ID]; !ok {
   558  				newContainer.t = &tomb.Tomb{}
   559  				newContainer.logger = d.logger.WithFields(log.Fields{"container_name": newContainer.Name})
   560  				newContainer.t.Go(func() error {
   561  					return d.TailDocker(newContainer, outChan, deleteChan)
   562  				})
   563  				d.runningContainerState[newContainer.ID] = newContainer
   564  			}
   565  		case containerToDelete := <-deleteChan:
   566  			if containerConfig, ok := d.runningContainerState[containerToDelete.ID]; ok {
   567  				log.Infof("container acquisition stopped for container '%s'", containerConfig.Name)
   568  				containerConfig.t.Kill(nil)
   569  				delete(d.runningContainerState, containerToDelete.ID)
   570  			}
   571  		case <-d.t.Dying():
   572  			for idx, container := range d.runningContainerState {
   573  				if d.runningContainerState[idx].t.Alive() {
   574  					d.logger.Infof("killing tail for container %s", container.Name)
   575  					d.runningContainerState[idx].t.Kill(nil)
   576  					if err := d.runningContainerState[idx].t.Wait(); err != nil {
   577  						d.logger.Infof("error while waiting for death of %s : %s", container.Name, err)
   578  					}
   579  				}
   580  			}
   581  			d.runningContainerState = nil
   582  			d.logger.Debugf("routine cleanup done, return")
   583  			return nil
   584  		}
   585  	}
   586  }