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

     1  package journalctlacquisition
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"fmt"
     7  	"net/url"
     8  	"os/exec"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/prometheus/client_golang/prometheus"
    13  	log "github.com/sirupsen/logrus"
    14  	"gopkg.in/tomb.v2"
    15  	"gopkg.in/yaml.v2"
    16  
    17  	"github.com/crowdsecurity/go-cs-lib/trace"
    18  
    19  	"github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration"
    20  	"github.com/crowdsecurity/crowdsec/pkg/types"
    21  )
    22  
    23  type JournalCtlConfiguration struct {
    24  	configuration.DataSourceCommonCfg `yaml:",inline"`
    25  	Filters                           []string `yaml:"journalctl_filter"`
    26  }
    27  
    28  type JournalCtlSource struct {
    29  	metricsLevel int
    30  	config       JournalCtlConfiguration
    31  	logger       *log.Entry
    32  	src          string
    33  	args         []string
    34  }
    35  
    36  const journalctlCmd string = "journalctl"
    37  
    38  var (
    39  	journalctlArgsOneShot  = []string{}
    40  	journalctlArgstreaming = []string{"--follow", "-n", "0"}
    41  )
    42  
    43  var linesRead = prometheus.NewCounterVec(
    44  	prometheus.CounterOpts{
    45  		Name: "cs_journalctlsource_hits_total",
    46  		Help: "Total lines that were read.",
    47  	},
    48  	[]string{"source"})
    49  
    50  func readLine(scanner *bufio.Scanner, out chan string, errChan chan error) error {
    51  	for scanner.Scan() {
    52  		txt := scanner.Text()
    53  		out <- txt
    54  	}
    55  	if errChan != nil && scanner.Err() != nil {
    56  		errChan <- scanner.Err()
    57  		close(errChan)
    58  		// the error is already consumed by runJournalCtl
    59  		return nil //nolint:nilerr
    60  	}
    61  	if errChan != nil {
    62  		close(errChan)
    63  	}
    64  	return nil
    65  }
    66  
    67  func (j *JournalCtlSource) runJournalCtl(out chan types.Event, t *tomb.Tomb) error {
    68  	ctx, cancel := context.WithCancel(context.Background())
    69  
    70  	cmd := exec.CommandContext(ctx, journalctlCmd, j.args...)
    71  	stdout, err := cmd.StdoutPipe()
    72  	if err != nil {
    73  		cancel()
    74  		return fmt.Errorf("could not get journalctl stdout: %s", err)
    75  	}
    76  	stderr, err := cmd.StderrPipe()
    77  	if err != nil {
    78  		cancel()
    79  		return fmt.Errorf("could not get journalctl stderr: %s", err)
    80  	}
    81  
    82  	stderrChan := make(chan string)
    83  	stdoutChan := make(chan string)
    84  	errChan := make(chan error, 1)
    85  
    86  	logger := j.logger.WithField("src", j.src)
    87  
    88  	logger.Infof("Running journalctl command: %s %s", cmd.Path, cmd.Args)
    89  	err = cmd.Start()
    90  	if err != nil {
    91  		cancel()
    92  		logger.Errorf("could not start journalctl command : %s", err)
    93  		return err
    94  	}
    95  
    96  	stdoutscanner := bufio.NewScanner(stdout)
    97  
    98  	if stdoutscanner == nil {
    99  		cancel()
   100  		cmd.Wait()
   101  		return fmt.Errorf("failed to create stdout scanner")
   102  	}
   103  
   104  	stderrScanner := bufio.NewScanner(stderr)
   105  
   106  	if stderrScanner == nil {
   107  		cancel()
   108  		cmd.Wait()
   109  		return fmt.Errorf("failed to create stderr scanner")
   110  	}
   111  	t.Go(func() error {
   112  		return readLine(stdoutscanner, stdoutChan, errChan)
   113  	})
   114  	t.Go(func() error {
   115  		//looks like journalctl closes stderr quite early, so ignore its status (but not its output)
   116  		return readLine(stderrScanner, stderrChan, nil)
   117  	})
   118  
   119  	for {
   120  		select {
   121  		case <-t.Dying():
   122  			logger.Infof("journalctl datasource %s stopping", j.src)
   123  			cancel()
   124  			cmd.Wait() //avoid zombie process
   125  			return nil
   126  		case stdoutLine := <-stdoutChan:
   127  			l := types.Line{}
   128  			l.Raw = stdoutLine
   129  			logger.Debugf("getting one line : %s", l.Raw)
   130  			l.Labels = j.config.Labels
   131  			l.Time = time.Now().UTC()
   132  			l.Src = j.src
   133  			l.Process = true
   134  			l.Module = j.GetName()
   135  			if j.metricsLevel != configuration.METRICS_NONE {
   136  				linesRead.With(prometheus.Labels{"source": j.src}).Inc()
   137  			}
   138  			var evt types.Event
   139  			if !j.config.UseTimeMachine {
   140  				evt = types.Event{Line: l, Process: true, Type: types.LOG, ExpectMode: types.LIVE}
   141  			} else {
   142  				evt = types.Event{Line: l, Process: true, Type: types.LOG, ExpectMode: types.TIMEMACHINE}
   143  			}
   144  			out <- evt
   145  		case stderrLine := <-stderrChan:
   146  			logger.Warnf("Got stderr message : %s", stderrLine)
   147  			err := fmt.Errorf("journalctl error : %s", stderrLine)
   148  			t.Kill(err)
   149  		case errScanner, ok := <-errChan:
   150  			if !ok {
   151  				logger.Debugf("errChan is closed, quitting")
   152  				t.Kill(nil)
   153  			}
   154  			if errScanner != nil {
   155  				t.Kill(errScanner)
   156  			}
   157  		}
   158  	}
   159  }
   160  
   161  func (j *JournalCtlSource) GetUuid() string {
   162  	return j.config.UniqueId
   163  }
   164  
   165  func (j *JournalCtlSource) GetMetrics() []prometheus.Collector {
   166  	return []prometheus.Collector{linesRead}
   167  }
   168  
   169  func (j *JournalCtlSource) GetAggregMetrics() []prometheus.Collector {
   170  	return []prometheus.Collector{linesRead}
   171  }
   172  
   173  func (j *JournalCtlSource) UnmarshalConfig(yamlConfig []byte) error {
   174  	j.config = JournalCtlConfiguration{}
   175  	err := yaml.UnmarshalStrict(yamlConfig, &j.config)
   176  	if err != nil {
   177  		return fmt.Errorf("cannot parse JournalCtlSource configuration: %w", err)
   178  	}
   179  
   180  	if j.config.Mode == "" {
   181  		j.config.Mode = configuration.TAIL_MODE
   182  	}
   183  
   184  	var args []string
   185  	if j.config.Mode == configuration.TAIL_MODE {
   186  		args = journalctlArgstreaming
   187  	} else {
   188  		args = journalctlArgsOneShot
   189  	}
   190  
   191  	if len(j.config.Filters) == 0 {
   192  		return fmt.Errorf("journalctl_filter is required")
   193  	}
   194  	j.args = append(args, j.config.Filters...)
   195  	j.src = fmt.Sprintf("journalctl-%s", strings.Join(j.config.Filters, "."))
   196  
   197  	return nil
   198  }
   199  
   200  func (j *JournalCtlSource) Configure(yamlConfig []byte, logger *log.Entry, MetricsLevel int) error {
   201  	j.logger = logger
   202  	j.metricsLevel = MetricsLevel
   203  
   204  	err := j.UnmarshalConfig(yamlConfig)
   205  	if err != nil {
   206  		return err
   207  	}
   208  
   209  	return nil
   210  }
   211  
   212  func (j *JournalCtlSource) ConfigureByDSN(dsn string, labels map[string]string, logger *log.Entry, uuid string) error {
   213  	j.logger = logger
   214  	j.config = JournalCtlConfiguration{}
   215  	j.config.Mode = configuration.CAT_MODE
   216  	j.config.Labels = labels
   217  	j.config.UniqueId = uuid
   218  
   219  	//format for the DSN is : journalctl://filters=FILTER1&filters=FILTER2
   220  	if !strings.HasPrefix(dsn, "journalctl://") {
   221  		return fmt.Errorf("invalid DSN %s for journalctl source, must start with journalctl://", dsn)
   222  	}
   223  
   224  	qs := strings.TrimPrefix(dsn, "journalctl://")
   225  	if len(qs) == 0 {
   226  		return fmt.Errorf("empty journalctl:// DSN")
   227  	}
   228  
   229  	params, err := url.ParseQuery(qs)
   230  	if err != nil {
   231  		return fmt.Errorf("could not parse journalctl DSN : %s", err)
   232  	}
   233  	for key, value := range params {
   234  		switch key {
   235  		case "filters":
   236  			j.config.Filters = append(j.config.Filters, value...)
   237  		case "log_level":
   238  			if len(value) != 1 {
   239  				return fmt.Errorf("expected zero or one value for 'log_level'")
   240  			}
   241  			lvl, err := log.ParseLevel(value[0])
   242  			if err != nil {
   243  				return fmt.Errorf("unknown level %s: %w", value[0], err)
   244  			}
   245  			j.logger.Logger.SetLevel(lvl)
   246  		case "since":
   247  			j.args = append(j.args, "--since", value[0])
   248  		default:
   249  			return fmt.Errorf("unsupported key %s in journalctl DSN", key)
   250  		}
   251  	}
   252  	j.args = append(j.args, j.config.Filters...)
   253  	return nil
   254  }
   255  
   256  func (j *JournalCtlSource) GetMode() string {
   257  	return j.config.Mode
   258  }
   259  
   260  func (j *JournalCtlSource) GetName() string {
   261  	return "journalctl"
   262  }
   263  
   264  func (j *JournalCtlSource) OneShotAcquisition(out chan types.Event, t *tomb.Tomb) error {
   265  	defer trace.CatchPanic("crowdsec/acquis/journalctl/oneshot")
   266  	err := j.runJournalCtl(out, t)
   267  	j.logger.Debug("Oneshot journalctl acquisition is done")
   268  	return err
   269  
   270  }
   271  
   272  func (j *JournalCtlSource) StreamingAcquisition(out chan types.Event, t *tomb.Tomb) error {
   273  	t.Go(func() error {
   274  		defer trace.CatchPanic("crowdsec/acquis/journalctl/streaming")
   275  		return j.runJournalCtl(out, t)
   276  	})
   277  	return nil
   278  }
   279  func (j *JournalCtlSource) CanRun() error {
   280  	//TODO: add a more precise check on version or something ?
   281  	_, err := exec.LookPath(journalctlCmd)
   282  	return err
   283  }
   284  func (j *JournalCtlSource) Dump() interface{} {
   285  	return j
   286  }