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 }