explain.go 6.3 KB

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