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 }