explain.go 6.6 KB

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