github.com/crowdsecurity/crowdsec@v1.6.1/cmd/crowdsec-cli/explain.go (about)

     1  package main
     2  
     3  import (
     4  	"bufio"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"os/exec"
    10  	"path/filepath"
    11  
    12  	log "github.com/sirupsen/logrus"
    13  	"github.com/spf13/cobra"
    14  
    15  	"github.com/crowdsecurity/crowdsec/pkg/dumps"
    16  	"github.com/crowdsecurity/crowdsec/pkg/hubtest"
    17  )
    18  
    19  func getLineCountForFile(filepath string) (int, error) {
    20  	f, err := os.Open(filepath)
    21  	if err != nil {
    22  		return 0, err
    23  	}
    24  	defer f.Close()
    25  
    26  	lc := 0
    27  	fs := bufio.NewReader(f)
    28  
    29  	for {
    30  		input, err := fs.ReadBytes('\n')
    31  		if len(input) > 1 {
    32  			lc++
    33  		}
    34  
    35  		if err != nil && err == io.EOF {
    36  			break
    37  		}
    38  	}
    39  
    40  	return lc, nil
    41  }
    42  
    43  type cliExplain struct {
    44  	cfg   configGetter
    45  	flags struct {
    46  		logFile               string
    47  		dsn                   string
    48  		logLine               string
    49  		logType               string
    50  		details               bool
    51  		skipOk                bool
    52  		onlySuccessfulParsers bool
    53  		noClean               bool
    54  		crowdsec              string
    55  		labels                string
    56  	}
    57  }
    58  
    59  func NewCLIExplain(cfg configGetter) *cliExplain {
    60  	return &cliExplain{
    61  		cfg: cfg,
    62  	}
    63  }
    64  
    65  func (cli *cliExplain) NewCommand() *cobra.Command {
    66  	cmd := &cobra.Command{
    67  		Use:   "explain",
    68  		Short: "Explain log pipeline",
    69  		Long: `
    70  Explain log pipeline 
    71  		`,
    72  		Example: `
    73  cscli explain --file ./myfile.log --type nginx 
    74  cscli explain --log "Sep 19 18:33:22 scw-d95986 sshd[24347]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=1.2.3.4" --type syslog
    75  cscli explain --dsn "file://myfile.log" --type nginx
    76  tail -n 5 myfile.log | cscli explain --type nginx -f -
    77  		`,
    78  		Args:              cobra.ExactArgs(0),
    79  		DisableAutoGenTag: true,
    80  		RunE: func(_ *cobra.Command, _ []string) error {
    81  			return cli.run()
    82  		},
    83  		PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
    84  			fileInfo, _ := os.Stdin.Stat()
    85  			if cli.flags.logFile == "-" && ((fileInfo.Mode() & os.ModeCharDevice) == os.ModeCharDevice) {
    86  				return fmt.Errorf("the option -f - is intended to work with pipes")
    87  			}
    88  
    89  			return nil
    90  		},
    91  	}
    92  
    93  	flags := cmd.Flags()
    94  
    95  	flags.StringVarP(&cli.flags.logFile, "file", "f", "", "Log file to test")
    96  	flags.StringVarP(&cli.flags.dsn, "dsn", "d", "", "DSN to test")
    97  	flags.StringVarP(&cli.flags.logLine, "log", "l", "", "Log line to test")
    98  	flags.StringVarP(&cli.flags.logType, "type", "t", "", "Type of the acquisition to test")
    99  	flags.StringVar(&cli.flags.labels, "labels", "", "Additional labels to add to the acquisition format (key:value,key2:value2)")
   100  	flags.BoolVarP(&cli.flags.details, "verbose", "v", false, "Display individual changes")
   101  	flags.BoolVar(&cli.flags.skipOk, "failures", false, "Only show failed lines")
   102  	flags.BoolVar(&cli.flags.onlySuccessfulParsers, "only-successful-parsers", false, "Only show successful parsers")
   103  	flags.StringVar(&cli.flags.crowdsec, "crowdsec", "crowdsec", "Path to crowdsec")
   104  	flags.BoolVar(&cli.flags.noClean, "no-clean", false, "Don't clean runtime environment after tests")
   105  
   106  	cmd.MarkFlagRequired("type")
   107  	cmd.MarkFlagsOneRequired("log", "file", "dsn")
   108  
   109  	return cmd
   110  }
   111  
   112  func (cli *cliExplain) run() error {
   113  	logFile := cli.flags.logFile
   114  	logLine := cli.flags.logLine
   115  	logType := cli.flags.logType
   116  	dsn := cli.flags.dsn
   117  	labels := cli.flags.labels
   118  	crowdsec := cli.flags.crowdsec
   119  
   120  	opts := dumps.DumpOpts{
   121  		Details:          cli.flags.details,
   122  		SkipOk:           cli.flags.skipOk,
   123  		ShowNotOkParsers: !cli.flags.onlySuccessfulParsers,
   124  	}
   125  
   126  	var f *os.File
   127  
   128  	// using empty string fallback to /tmp
   129  	dir, err := os.MkdirTemp("", "cscli_explain")
   130  	if err != nil {
   131  		return fmt.Errorf("couldn't create a temporary directory to store cscli explain result: %w", err)
   132  	}
   133  
   134  	defer func() {
   135  		if cli.flags.noClean {
   136  			return
   137  		}
   138  
   139  		if _, err := os.Stat(dir); !os.IsNotExist(err) {
   140  			if err := os.RemoveAll(dir); err != nil {
   141  				log.Errorf("unable to delete temporary directory '%s': %s", dir, err)
   142  			}
   143  		}
   144  	}()
   145  
   146  	// we create a  temporary log file if a log line/stdin has been provided
   147  	if logLine != "" || logFile == "-" {
   148  		tmpFile := filepath.Join(dir, "cscli_test_tmp.log")
   149  
   150  		f, err = os.Create(tmpFile)
   151  		if err != nil {
   152  			return err
   153  		}
   154  
   155  		if logLine != "" {
   156  			_, err = f.WriteString(logLine)
   157  			if err != nil {
   158  				return err
   159  			}
   160  		} else if logFile == "-" {
   161  			reader := bufio.NewReader(os.Stdin)
   162  			errCount := 0
   163  			for {
   164  				input, err := reader.ReadBytes('\n')
   165  				if err != nil && errors.Is(err, io.EOF) {
   166  					break
   167  				}
   168  				if len(input) > 1 {
   169  					_, err = f.Write(input)
   170  				}
   171  				if err != nil || len(input) <= 1 {
   172  					errCount++
   173  				}
   174  			}
   175  			if errCount > 0 {
   176  				log.Warnf("Failed to write %d lines to %s", errCount, tmpFile)
   177  			}
   178  		}
   179  
   180  		f.Close()
   181  		// this is the file that was going to be read by crowdsec anyway
   182  		logFile = tmpFile
   183  	}
   184  
   185  	if logFile != "" {
   186  		absolutePath, err := filepath.Abs(logFile)
   187  		if err != nil {
   188  			return fmt.Errorf("unable to get absolute path of '%s', exiting", logFile)
   189  		}
   190  
   191  		dsn = fmt.Sprintf("file://%s", absolutePath)
   192  
   193  		lineCount, err := getLineCountForFile(absolutePath)
   194  		if err != nil {
   195  			return err
   196  		}
   197  
   198  		log.Debugf("file %s has %d lines", absolutePath, lineCount)
   199  
   200  		if lineCount == 0 {
   201  			return fmt.Errorf("the log file is empty: %s", absolutePath)
   202  		}
   203  
   204  		if lineCount > 100 {
   205  			log.Warnf("%s contains %d lines. This may take a lot of resources.", absolutePath, lineCount)
   206  		}
   207  	}
   208  
   209  	if dsn == "" {
   210  		return fmt.Errorf("no acquisition (--file or --dsn) provided, can't run cscli test")
   211  	}
   212  
   213  	cmdArgs := []string{"-c", ConfigFilePath, "-type", logType, "-dsn", dsn, "-dump-data", dir, "-no-api"}
   214  
   215  	if labels != "" {
   216  		log.Debugf("adding labels %s", labels)
   217  		cmdArgs = append(cmdArgs, "-label", labels)
   218  	}
   219  
   220  	crowdsecCmd := exec.Command(crowdsec, cmdArgs...)
   221  
   222  	output, err := crowdsecCmd.CombinedOutput()
   223  	if err != nil {
   224  		fmt.Println(string(output))
   225  
   226  		return fmt.Errorf("fail to run crowdsec for test: %w", err)
   227  	}
   228  
   229  	parserDumpFile := filepath.Join(dir, hubtest.ParserResultFileName)
   230  	bucketStateDumpFile := filepath.Join(dir, hubtest.BucketPourResultFileName)
   231  
   232  	parserDump, err := dumps.LoadParserDump(parserDumpFile)
   233  	if err != nil {
   234  		return fmt.Errorf("unable to load parser dump result: %w", err)
   235  	}
   236  
   237  	bucketStateDump, err := dumps.LoadBucketPourDump(bucketStateDumpFile)
   238  	if err != nil {
   239  		return fmt.Errorf("unable to load bucket dump result: %w", err)
   240  	}
   241  
   242  	dumps.DumpTree(*parserDump, *bucketStateDump, opts)
   243  
   244  	return nil
   245  }