file_test.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. package fileacquisition
  2. import (
  3. "fmt"
  4. "os"
  5. "runtime"
  6. "testing"
  7. "time"
  8. "github.com/crowdsecurity/crowdsec/pkg/cstest"
  9. "github.com/crowdsecurity/crowdsec/pkg/types"
  10. log "github.com/sirupsen/logrus"
  11. "github.com/sirupsen/logrus/hooks/test"
  12. "github.com/stretchr/testify/assert"
  13. "gopkg.in/tomb.v2"
  14. )
  15. func TestBadConfiguration(t *testing.T) {
  16. tests := []struct {
  17. config string
  18. expectedErr string
  19. }{
  20. {
  21. config: `foobar: asd.log`,
  22. expectedErr: "line 1: field foobar not found in type fileacquisition.FileConfiguration",
  23. },
  24. {
  25. config: `mode: tail`,
  26. expectedErr: "no filename or filenames configuration provided",
  27. },
  28. {
  29. config: `filename: "[asd-.log"`,
  30. expectedErr: "Glob failure: syntax error in pattern",
  31. },
  32. }
  33. subLogger := log.WithFields(log.Fields{
  34. "type": "file",
  35. })
  36. for _, test := range tests {
  37. f := FileSource{}
  38. err := f.Configure([]byte(test.config), subLogger)
  39. assert.Contains(t, err.Error(), test.expectedErr)
  40. }
  41. }
  42. func TestConfigureDSN(t *testing.T) {
  43. var file string
  44. if runtime.GOOS != "windows" {
  45. file = "/etc/passwd"
  46. } else {
  47. file = "C:\\Windows\\System32\\drivers\\etc\\hosts"
  48. }
  49. tests := []struct {
  50. dsn string
  51. expectedErr string
  52. }{
  53. {
  54. dsn: "asd://",
  55. expectedErr: "invalid DSN asd:// for file source, must start with file://",
  56. },
  57. {
  58. dsn: "file://",
  59. expectedErr: "empty file:// DSN",
  60. },
  61. {
  62. dsn: fmt.Sprintf("file://%s?log_level=warn", file),
  63. expectedErr: "",
  64. },
  65. {
  66. dsn: fmt.Sprintf("file://%s?log_level=foobar", file),
  67. expectedErr: "unknown level foobar: not a valid logrus Level:",
  68. },
  69. }
  70. subLogger := log.WithFields(log.Fields{
  71. "type": "file",
  72. })
  73. for _, test := range tests {
  74. f := FileSource{}
  75. err := f.ConfigureByDSN(test.dsn, map[string]string{"type": "testtype"}, subLogger)
  76. cstest.AssertErrorContains(t, err, test.expectedErr)
  77. }
  78. }
  79. func TestOneShot(t *testing.T) {
  80. var permDeniedFile string
  81. var permDeniedError string
  82. if runtime.GOOS != "windows" {
  83. permDeniedFile = "/etc/shadow"
  84. permDeniedError = "failed opening /etc/shadow: open /etc/shadow: permission denied"
  85. } else {
  86. //Technically, this is not a permission denied error, but we just want to test what happens
  87. //if we do not have access to the file
  88. permDeniedFile = "C:\\Windows\\System32\\config\\SAM"
  89. permDeniedError = "failed opening C:\\Windows\\System32\\config\\SAM: open C:\\Windows\\System32\\config\\SAM: The process cannot access the file because it is being used by another process."
  90. }
  91. tests := []struct {
  92. config string
  93. expectedConfigErr string
  94. expectedErr string
  95. expectedOutput string
  96. expectedLines int
  97. logLevel log.Level
  98. setup func()
  99. afterConfigure func()
  100. teardown func()
  101. }{
  102. {
  103. config: fmt.Sprintf(`
  104. mode: cat
  105. filename: %s`, permDeniedFile),
  106. expectedConfigErr: "",
  107. expectedErr: permDeniedError,
  108. expectedOutput: "",
  109. logLevel: log.WarnLevel,
  110. expectedLines: 0,
  111. },
  112. {
  113. config: `
  114. mode: cat
  115. filename: /`,
  116. expectedConfigErr: "",
  117. expectedErr: "",
  118. expectedOutput: "/ is a directory, ignoring it",
  119. logLevel: log.WarnLevel,
  120. expectedLines: 0,
  121. },
  122. {
  123. config: `
  124. mode: cat
  125. filename: "[*-.log"`,
  126. expectedConfigErr: "Glob failure: syntax error in pattern",
  127. expectedErr: "",
  128. expectedOutput: "",
  129. logLevel: log.WarnLevel,
  130. expectedLines: 0,
  131. },
  132. {
  133. config: `
  134. mode: cat
  135. filename: /do/not/exist`,
  136. expectedConfigErr: "",
  137. expectedErr: "",
  138. expectedOutput: "No matching files for pattern /do/not/exist",
  139. logLevel: log.WarnLevel,
  140. expectedLines: 0,
  141. },
  142. {
  143. config: `
  144. mode: cat
  145. filename: test_files/test.log`,
  146. expectedConfigErr: "",
  147. expectedErr: "",
  148. expectedOutput: "",
  149. expectedLines: 5,
  150. logLevel: log.WarnLevel,
  151. },
  152. {
  153. config: `
  154. mode: cat
  155. filename: test_files/test.log.gz`,
  156. expectedConfigErr: "",
  157. expectedErr: "",
  158. expectedOutput: "",
  159. expectedLines: 5,
  160. logLevel: log.WarnLevel,
  161. },
  162. {
  163. config: `
  164. mode: cat
  165. filename: test_files/bad.gz`,
  166. expectedConfigErr: "",
  167. expectedErr: "failed to read gz test_files/bad.gz: unexpected EOF",
  168. expectedOutput: "",
  169. expectedLines: 0,
  170. logLevel: log.WarnLevel,
  171. },
  172. {
  173. config: `
  174. mode: cat
  175. filename: test_files/test_delete.log`,
  176. setup: func() {
  177. f, _ := os.Create("test_files/test_delete.log")
  178. f.Close()
  179. },
  180. afterConfigure: func() {
  181. os.Remove("test_files/test_delete.log")
  182. },
  183. expectedErr: "could not stat file test_files/test_delete.log",
  184. },
  185. }
  186. for _, ts := range tests {
  187. logger, hook := test.NewNullLogger()
  188. logger.SetLevel(ts.logLevel)
  189. subLogger := logger.WithFields(log.Fields{
  190. "type": "file",
  191. })
  192. tomb := tomb.Tomb{}
  193. out := make(chan types.Event)
  194. f := FileSource{}
  195. if ts.setup != nil {
  196. ts.setup()
  197. }
  198. err := f.Configure([]byte(ts.config), subLogger)
  199. cstest.AssertErrorContains(t, err, ts.expectedConfigErr)
  200. if err != nil {
  201. continue
  202. }
  203. if ts.afterConfigure != nil {
  204. ts.afterConfigure()
  205. }
  206. actualLines := 0
  207. if ts.expectedLines != 0 {
  208. go func() {
  209. READLOOP:
  210. for {
  211. select {
  212. case <-out:
  213. actualLines++
  214. case <-time.After(1 * time.Second):
  215. break READLOOP
  216. }
  217. }
  218. }()
  219. }
  220. err = f.OneShotAcquisition(out, &tomb)
  221. cstest.AssertErrorContains(t, err, ts.expectedErr)
  222. if ts.expectedLines != 0 {
  223. assert.Equal(t, actualLines, ts.expectedLines)
  224. }
  225. if ts.expectedOutput != "" {
  226. assert.Contains(t, hook.LastEntry().Message, ts.expectedOutput)
  227. hook.Reset()
  228. }
  229. if ts.teardown != nil {
  230. ts.teardown()
  231. }
  232. }
  233. }
  234. func TestLiveAcquisition(t *testing.T) {
  235. var permDeniedFile string
  236. var permDeniedError string
  237. var testPattern string
  238. if runtime.GOOS != "windows" {
  239. permDeniedFile = "/etc/shadow"
  240. permDeniedError = "unable to read /etc/shadow : open /etc/shadow: permission denied"
  241. testPattern = "test_files/*.log"
  242. } else {
  243. //Technically, this is not a permission denied error, but we just want to test what happens
  244. //if we do not have access to the file
  245. permDeniedFile = "C:\\Windows\\System32\\config\\SAM"
  246. permDeniedError = "unable to read C:\\Windows\\System32\\config\\SAM : open C:\\Windows\\System32\\config\\SAM: The process cannot access the file because it is being used by another process"
  247. testPattern = "test_files\\\\*.log" // the \ must be escaped twice: once for the string, once for the yaml config
  248. }
  249. tests := []struct {
  250. config string
  251. expectedErr string
  252. expectedOutput string
  253. name string
  254. expectedLines int
  255. logLevel log.Level
  256. setup func()
  257. afterConfigure func()
  258. teardown func()
  259. }{
  260. {
  261. config: fmt.Sprintf(`
  262. mode: tail
  263. filename: %s`, permDeniedFile),
  264. expectedErr: "",
  265. expectedOutput: permDeniedError,
  266. logLevel: log.InfoLevel,
  267. expectedLines: 0,
  268. name: "PermissionDenied",
  269. },
  270. {
  271. config: `
  272. mode: tail
  273. filename: /`,
  274. expectedErr: "",
  275. expectedOutput: "/ is a directory, ignoring it",
  276. logLevel: log.WarnLevel,
  277. expectedLines: 0,
  278. name: "Directory",
  279. },
  280. {
  281. config: `
  282. mode: tail
  283. filename: /do/not/exist`,
  284. expectedErr: "",
  285. expectedOutput: "No matching files for pattern /do/not/exist",
  286. logLevel: log.WarnLevel,
  287. expectedLines: 0,
  288. name: "badPattern",
  289. },
  290. {
  291. config: fmt.Sprintf(`
  292. mode: tail
  293. filenames:
  294. - %s
  295. force_inotify: true`, testPattern),
  296. expectedErr: "",
  297. expectedOutput: "",
  298. expectedLines: 5,
  299. logLevel: log.DebugLevel,
  300. name: "basicGlob",
  301. },
  302. {
  303. config: fmt.Sprintf(`
  304. mode: tail
  305. filenames:
  306. - %s
  307. force_inotify: true`, testPattern),
  308. expectedErr: "",
  309. expectedOutput: "",
  310. expectedLines: 0,
  311. logLevel: log.DebugLevel,
  312. name: "GlobInotify",
  313. afterConfigure: func() {
  314. f, _ := os.Create("test_files/a.log")
  315. f.Close()
  316. time.Sleep(1 * time.Second)
  317. os.Remove("test_files/a.log")
  318. },
  319. },
  320. {
  321. config: fmt.Sprintf(`
  322. mode: tail
  323. filenames:
  324. - %s
  325. force_inotify: true`, testPattern),
  326. expectedErr: "",
  327. expectedOutput: "",
  328. expectedLines: 5,
  329. logLevel: log.DebugLevel,
  330. name: "GlobInotifyChmod",
  331. afterConfigure: func() {
  332. f, _ := os.Create("test_files/a.log")
  333. f.Close()
  334. time.Sleep(1 * time.Second)
  335. os.Chmod("test_files/a.log", 0000)
  336. },
  337. teardown: func() {
  338. os.Chmod("test_files/a.log", 0644)
  339. os.Remove("test_files/a.log")
  340. },
  341. },
  342. {
  343. config: fmt.Sprintf(`
  344. mode: tail
  345. filenames:
  346. - %s
  347. force_inotify: true`, testPattern),
  348. expectedErr: "",
  349. expectedOutput: "",
  350. expectedLines: 5,
  351. logLevel: log.DebugLevel,
  352. name: "InotifyMkDir",
  353. afterConfigure: func() {
  354. os.Mkdir("test_files/pouet/", 0700)
  355. },
  356. teardown: func() {
  357. os.Remove("test_files/pouet/")
  358. },
  359. },
  360. }
  361. for _, ts := range tests {
  362. t.Logf("test: %s", ts.name)
  363. logger, hook := test.NewNullLogger()
  364. logger.SetLevel(ts.logLevel)
  365. subLogger := logger.WithFields(log.Fields{
  366. "type": "file",
  367. })
  368. tomb := tomb.Tomb{}
  369. out := make(chan types.Event)
  370. f := FileSource{}
  371. if ts.setup != nil {
  372. ts.setup()
  373. }
  374. err := f.Configure([]byte(ts.config), subLogger)
  375. if err != nil {
  376. t.Fatalf("Unexpected error : %s", err)
  377. }
  378. if ts.afterConfigure != nil {
  379. ts.afterConfigure()
  380. }
  381. actualLines := 0
  382. if ts.expectedLines != 0 {
  383. go func() {
  384. READLOOP:
  385. for {
  386. select {
  387. case <-out:
  388. actualLines++
  389. case <-time.After(2 * time.Second):
  390. break READLOOP
  391. }
  392. }
  393. }()
  394. }
  395. err = f.StreamingAcquisition(out, &tomb)
  396. cstest.AssertErrorContains(t, err, ts.expectedErr)
  397. if ts.expectedLines != 0 {
  398. fd, err := os.Create("test_files/stream.log")
  399. if err != nil {
  400. t.Fatalf("could not create test file : %s", err)
  401. }
  402. for i := 0; i < 5; i++ {
  403. _, err = fd.WriteString(fmt.Sprintf("%d\n", i))
  404. if err != nil {
  405. t.Fatalf("could not write test file : %s", err)
  406. os.Remove("test_files/stream.log")
  407. }
  408. }
  409. fd.Close()
  410. //we sleep to make sure we detect the new file
  411. time.Sleep(1 * time.Second)
  412. os.Remove("test_files/stream.log")
  413. assert.Equal(t, ts.expectedLines, actualLines)
  414. }
  415. if ts.expectedOutput != "" {
  416. if hook.LastEntry() == nil {
  417. t.Fatalf("expected output %s, but got nothing", ts.expectedOutput)
  418. }
  419. assert.Contains(t, hook.LastEntry().Message, ts.expectedOutput)
  420. hook.Reset()
  421. }
  422. if ts.teardown != nil {
  423. ts.teardown()
  424. }
  425. tomb.Kill(nil)
  426. }
  427. }