apic_test.go 31 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109
  1. package apiserver
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "fmt"
  7. "net/http"
  8. "net/url"
  9. "os"
  10. "reflect"
  11. "sync"
  12. "testing"
  13. "time"
  14. "github.com/jarcoal/httpmock"
  15. "github.com/sirupsen/logrus"
  16. "github.com/stretchr/testify/assert"
  17. "github.com/stretchr/testify/require"
  18. "gopkg.in/tomb.v2"
  19. "github.com/crowdsecurity/crowdsec/pkg/apiclient"
  20. "github.com/crowdsecurity/crowdsec/pkg/csconfig"
  21. "github.com/crowdsecurity/crowdsec/pkg/cstest"
  22. "github.com/crowdsecurity/crowdsec/pkg/cwversion"
  23. "github.com/crowdsecurity/crowdsec/pkg/database"
  24. "github.com/crowdsecurity/crowdsec/pkg/database/ent/decision"
  25. "github.com/crowdsecurity/crowdsec/pkg/database/ent/machine"
  26. "github.com/crowdsecurity/crowdsec/pkg/models"
  27. "github.com/crowdsecurity/crowdsec/pkg/modelscapi"
  28. "github.com/crowdsecurity/crowdsec/pkg/types"
  29. )
  30. func getDBClient(t *testing.T) *database.Client {
  31. t.Helper()
  32. dbPath, err := os.CreateTemp("", "*sqlite")
  33. require.NoError(t, err)
  34. dbClient, err := database.NewClient(&csconfig.DatabaseCfg{
  35. Type: "sqlite",
  36. DbName: "crowdsec",
  37. DbPath: dbPath.Name(),
  38. })
  39. require.NoError(t, err)
  40. return dbClient
  41. }
  42. func getAPIC(t *testing.T) *apic {
  43. t.Helper()
  44. dbClient := getDBClient(t)
  45. return &apic{
  46. AlertsAddChan: make(chan []*models.Alert),
  47. //DecisionDeleteChan: make(chan []*models.Decision),
  48. dbClient: dbClient,
  49. mu: sync.Mutex{},
  50. startup: true,
  51. pullTomb: tomb.Tomb{},
  52. pushTomb: tomb.Tomb{},
  53. metricsTomb: tomb.Tomb{},
  54. scenarioList: make([]string, 0),
  55. consoleConfig: &csconfig.ConsoleConfig{
  56. ShareManualDecisions: types.BoolPtr(false),
  57. ShareTaintedScenarios: types.BoolPtr(false),
  58. ShareCustomScenarios: types.BoolPtr(false),
  59. ShareContext: types.BoolPtr(false),
  60. },
  61. }
  62. }
  63. func absDiff(a int, b int) (c int) {
  64. if c = a - b; c < 0 {
  65. return -1 * c
  66. }
  67. return c
  68. }
  69. func assertTotalDecisionCount(t *testing.T, dbClient *database.Client, count int) {
  70. d := dbClient.Ent.Decision.Query().AllX(context.Background())
  71. assert.Len(t, d, count)
  72. }
  73. func assertTotalValidDecisionCount(t *testing.T, dbClient *database.Client, count int) {
  74. d := dbClient.Ent.Decision.Query().Where(
  75. decision.UntilGT(time.Now()),
  76. ).AllX(context.Background())
  77. assert.Len(t, d, count)
  78. }
  79. func jsonMarshalX(v interface{}) []byte {
  80. data, err := json.Marshal(v)
  81. if err != nil {
  82. panic(err)
  83. }
  84. return data
  85. }
  86. func assertTotalAlertCount(t *testing.T, dbClient *database.Client, count int) {
  87. d := dbClient.Ent.Alert.Query().AllX(context.Background())
  88. assert.Len(t, d, count)
  89. }
  90. func TestAPICCAPIPullIsOld(t *testing.T) {
  91. api := getAPIC(t)
  92. isOld, err := api.CAPIPullIsOld()
  93. require.NoError(t, err)
  94. assert.True(t, isOld)
  95. decision := api.dbClient.Ent.Decision.Create().
  96. SetUntil(time.Now().Add(time.Hour)).
  97. SetScenario("crowdsec/test").
  98. SetType("IP").
  99. SetScope("Country").
  100. SetValue("Blah").
  101. SetOrigin(types.CAPIOrigin).
  102. SaveX(context.Background())
  103. api.dbClient.Ent.Alert.Create().
  104. SetCreatedAt(time.Now()).
  105. SetScenario("crowdsec/test").
  106. AddDecisions(
  107. decision,
  108. ).
  109. SaveX(context.Background())
  110. isOld, err = api.CAPIPullIsOld()
  111. require.NoError(t, err)
  112. assert.False(t, isOld)
  113. }
  114. func TestAPICFetchScenariosListFromDB(t *testing.T) {
  115. tests := []struct {
  116. name string
  117. machineIDsWithScenarios map[string]string
  118. expectedScenarios []string
  119. }{
  120. {
  121. name: "Simple one machine with two scenarios",
  122. machineIDsWithScenarios: map[string]string{
  123. "a": "crowdsecurity/http-bf,crowdsecurity/ssh-bf",
  124. },
  125. expectedScenarios: []string{"crowdsecurity/ssh-bf", "crowdsecurity/http-bf"},
  126. },
  127. {
  128. name: "Multi machine with custom+hub scenarios",
  129. machineIDsWithScenarios: map[string]string{
  130. "a": "crowdsecurity/http-bf,crowdsecurity/ssh-bf,my_scenario",
  131. "b": "crowdsecurity/http-bf,crowdsecurity/ssh-bf,foo_scenario",
  132. },
  133. expectedScenarios: []string{"crowdsecurity/ssh-bf", "crowdsecurity/http-bf", "my_scenario", "foo_scenario"},
  134. },
  135. }
  136. for _, tc := range tests {
  137. tc := tc
  138. t.Run(tc.name, func(t *testing.T) {
  139. api := getAPIC(t)
  140. for machineID, scenarios := range tc.machineIDsWithScenarios {
  141. api.dbClient.Ent.Machine.Create().
  142. SetMachineId(machineID).
  143. SetPassword(testPassword.String()).
  144. SetIpAddress("1.2.3.4").
  145. SetScenarios(scenarios).
  146. ExecX(context.Background())
  147. }
  148. scenarios, err := api.FetchScenariosListFromDB()
  149. for machineID := range tc.machineIDsWithScenarios {
  150. api.dbClient.Ent.Machine.Delete().Where(machine.MachineIdEQ(machineID)).ExecX(context.Background())
  151. }
  152. require.NoError(t, err)
  153. assert.ElementsMatch(t, tc.expectedScenarios, scenarios)
  154. })
  155. }
  156. }
  157. func TestNewAPIC(t *testing.T) {
  158. var testConfig *csconfig.OnlineApiClientCfg
  159. setConfig := func() {
  160. testConfig = &csconfig.OnlineApiClientCfg{
  161. Credentials: &csconfig.ApiCredentialsCfg{
  162. URL: "http://foobar/",
  163. Login: "foo",
  164. Password: "bar",
  165. },
  166. }
  167. }
  168. type args struct {
  169. dbClient *database.Client
  170. consoleConfig *csconfig.ConsoleConfig
  171. }
  172. tests := []struct {
  173. name string
  174. args args
  175. expectedErr string
  176. action func()
  177. }{
  178. {
  179. name: "simple",
  180. action: func() {},
  181. args: args{
  182. dbClient: getDBClient(t),
  183. consoleConfig: LoadTestConfig().API.Server.ConsoleConfig,
  184. },
  185. },
  186. {
  187. name: "error in parsing URL",
  188. action: func() { testConfig.Credentials.URL = "foobar http://" },
  189. args: args{
  190. dbClient: getDBClient(t),
  191. consoleConfig: LoadTestConfig().API.Server.ConsoleConfig,
  192. },
  193. expectedErr: "first path segment in URL cannot contain colon",
  194. },
  195. }
  196. for _, tc := range tests {
  197. tc := tc
  198. t.Run(tc.name, func(t *testing.T) {
  199. setConfig()
  200. httpmock.Activate()
  201. defer httpmock.DeactivateAndReset()
  202. httpmock.RegisterResponder("POST", "http://foobar/v3/watchers/login", httpmock.NewBytesResponder(
  203. 200, jsonMarshalX(
  204. models.WatcherAuthResponse{
  205. Code: 200,
  206. Expire: "2023-01-12T22:51:43Z",
  207. Token: "MyToken",
  208. },
  209. ),
  210. ))
  211. tc.action()
  212. _, err := NewAPIC(testConfig, tc.args.dbClient, tc.args.consoleConfig)
  213. cstest.RequireErrorContains(t, err, tc.expectedErr)
  214. })
  215. }
  216. }
  217. func TestAPICHandleDeletedDecisions(t *testing.T) {
  218. api := getAPIC(t)
  219. _, deleteCounters := makeAddAndDeleteCounters()
  220. decision1 := api.dbClient.Ent.Decision.Create().
  221. SetUntil(time.Now().Add(time.Hour)).
  222. SetScenario("crowdsec/test").
  223. SetType("ban").
  224. SetScope("IP").
  225. SetValue("1.2.3.4").
  226. SetOrigin(types.CAPIOrigin).
  227. SaveX(context.Background())
  228. api.dbClient.Ent.Decision.Create().
  229. SetUntil(time.Now().Add(time.Hour)).
  230. SetScenario("crowdsec/test").
  231. SetType("ban").
  232. SetScope("IP").
  233. SetValue("1.2.3.4").
  234. SetOrigin(types.CAPIOrigin).
  235. SaveX(context.Background())
  236. assertTotalDecisionCount(t, api.dbClient, 2)
  237. nbDeleted, err := api.HandleDeletedDecisions([]*models.Decision{{
  238. Value: types.StrPtr("1.2.3.4"),
  239. Origin: types.StrPtr(types.CAPIOrigin),
  240. Type: &decision1.Type,
  241. Scenario: types.StrPtr("crowdsec/test"),
  242. Scope: types.StrPtr("IP"),
  243. }}, deleteCounters)
  244. assert.NoError(t, err)
  245. assert.Equal(t, 2, nbDeleted)
  246. assert.Equal(t, 2, deleteCounters[types.CAPIOrigin]["all"])
  247. }
  248. func TestAPICGetMetrics(t *testing.T) {
  249. cleanUp := func(api *apic) {
  250. api.dbClient.Ent.Bouncer.Delete().ExecX(context.Background())
  251. api.dbClient.Ent.Machine.Delete().ExecX(context.Background())
  252. }
  253. tests := []struct {
  254. name string
  255. machineIDs []string
  256. bouncers []string
  257. expectedMetric *models.Metrics
  258. }{
  259. {
  260. name: "simple",
  261. machineIDs: []string{"a", "b", "c"},
  262. bouncers: []string{"1", "2", "3"},
  263. expectedMetric: &models.Metrics{
  264. ApilVersion: types.StrPtr(cwversion.VersionStr()),
  265. Bouncers: []*models.MetricsBouncerInfo{
  266. {
  267. CustomName: "1",
  268. LastPull: time.Time{}.String(),
  269. }, {
  270. CustomName: "2",
  271. LastPull: time.Time{}.String(),
  272. }, {
  273. CustomName: "3",
  274. LastPull: time.Time{}.String(),
  275. },
  276. },
  277. Machines: []*models.MetricsAgentInfo{
  278. {
  279. Name: "a",
  280. LastPush: time.Time{}.String(),
  281. LastUpdate: time.Time{}.String(),
  282. },
  283. {
  284. Name: "b",
  285. LastPush: time.Time{}.String(),
  286. LastUpdate: time.Time{}.String(),
  287. },
  288. {
  289. Name: "c",
  290. LastPush: time.Time{}.String(),
  291. LastUpdate: time.Time{}.String(),
  292. },
  293. },
  294. },
  295. },
  296. }
  297. for _, tc := range tests {
  298. tc := tc
  299. t.Run(tc.name, func(t *testing.T) {
  300. apiClient := getAPIC(t)
  301. cleanUp(apiClient)
  302. for i, machineID := range tc.machineIDs {
  303. apiClient.dbClient.Ent.Machine.Create().
  304. SetMachineId(machineID).
  305. SetPassword(testPassword.String()).
  306. SetIpAddress(fmt.Sprintf("1.2.3.%d", i)).
  307. SetScenarios("crowdsecurity/test").
  308. SetLastPush(time.Time{}).
  309. SetUpdatedAt(time.Time{}).
  310. ExecX(context.Background())
  311. }
  312. for i, bouncerName := range tc.bouncers {
  313. apiClient.dbClient.Ent.Bouncer.Create().
  314. SetIPAddress(fmt.Sprintf("1.2.3.%d", i)).
  315. SetName(bouncerName).
  316. SetAPIKey("foobar").
  317. SetRevoked(false).
  318. SetLastPull(time.Time{}).
  319. ExecX(context.Background())
  320. }
  321. foundMetrics, err := apiClient.GetMetrics()
  322. require.NoError(t, err)
  323. assert.Equal(t, tc.expectedMetric.Bouncers, foundMetrics.Bouncers)
  324. assert.Equal(t, tc.expectedMetric.Machines, foundMetrics.Machines)
  325. })
  326. }
  327. }
  328. func TestCreateAlertsForDecision(t *testing.T) {
  329. httpBfDecisionList := &models.Decision{
  330. Origin: types.StrPtr(types.ListOrigin),
  331. Scenario: types.StrPtr("crowdsecurity/http-bf"),
  332. }
  333. sshBfDecisionList := &models.Decision{
  334. Origin: types.StrPtr(types.ListOrigin),
  335. Scenario: types.StrPtr("crowdsecurity/ssh-bf"),
  336. }
  337. httpBfDecisionCommunity := &models.Decision{
  338. Origin: types.StrPtr(types.CAPIOrigin),
  339. Scenario: types.StrPtr("crowdsecurity/http-bf"),
  340. }
  341. sshBfDecisionCommunity := &models.Decision{
  342. Origin: types.StrPtr(types.CAPIOrigin),
  343. Scenario: types.StrPtr("crowdsecurity/ssh-bf"),
  344. }
  345. type args struct {
  346. decisions []*models.Decision
  347. }
  348. tests := []struct {
  349. name string
  350. args args
  351. want []*models.Alert
  352. }{
  353. {
  354. name: "2 decisions CAPI List Decisions should create 2 alerts",
  355. args: args{
  356. decisions: []*models.Decision{
  357. httpBfDecisionList,
  358. sshBfDecisionList,
  359. },
  360. },
  361. want: []*models.Alert{
  362. createAlertForDecision(httpBfDecisionList),
  363. createAlertForDecision(sshBfDecisionList),
  364. },
  365. },
  366. {
  367. name: "2 decisions CAPI List same scenario decisions should create 1 alert",
  368. args: args{
  369. decisions: []*models.Decision{
  370. httpBfDecisionList,
  371. httpBfDecisionList,
  372. },
  373. },
  374. want: []*models.Alert{
  375. createAlertForDecision(httpBfDecisionList),
  376. },
  377. },
  378. {
  379. name: "5 decisions from community list should create 1 alert",
  380. args: args{
  381. decisions: []*models.Decision{
  382. httpBfDecisionCommunity,
  383. httpBfDecisionCommunity,
  384. sshBfDecisionCommunity,
  385. sshBfDecisionCommunity,
  386. sshBfDecisionCommunity,
  387. },
  388. },
  389. want: []*models.Alert{
  390. createAlertForDecision(sshBfDecisionCommunity),
  391. },
  392. },
  393. }
  394. for _, tc := range tests {
  395. tc := tc
  396. t.Run(tc.name, func(t *testing.T) {
  397. if got := createAlertsForDecisions(tc.args.decisions); !reflect.DeepEqual(got, tc.want) {
  398. t.Errorf("createAlertsForDecisions() = %v, want %v", got, tc.want)
  399. }
  400. })
  401. }
  402. }
  403. func TestFillAlertsWithDecisions(t *testing.T) {
  404. httpBfDecisionCommunity := &models.Decision{
  405. Origin: types.StrPtr(types.CAPIOrigin),
  406. Scenario: types.StrPtr("crowdsecurity/http-bf"),
  407. Scope: types.StrPtr("ip"),
  408. }
  409. sshBfDecisionCommunity := &models.Decision{
  410. Origin: types.StrPtr(types.CAPIOrigin),
  411. Scenario: types.StrPtr("crowdsecurity/ssh-bf"),
  412. Scope: types.StrPtr("ip"),
  413. }
  414. httpBfDecisionList := &models.Decision{
  415. Origin: types.StrPtr(types.ListOrigin),
  416. Scenario: types.StrPtr("crowdsecurity/http-bf"),
  417. Scope: types.StrPtr("ip"),
  418. }
  419. sshBfDecisionList := &models.Decision{
  420. Origin: types.StrPtr(types.ListOrigin),
  421. Scenario: types.StrPtr("crowdsecurity/ssh-bf"),
  422. Scope: types.StrPtr("ip"),
  423. }
  424. type args struct {
  425. alerts []*models.Alert
  426. decisions []*models.Decision
  427. }
  428. tests := []struct {
  429. name string
  430. args args
  431. want []*models.Alert
  432. }{
  433. {
  434. name: "1 CAPI alert should pair up with n CAPI decisions",
  435. args: args{
  436. alerts: []*models.Alert{createAlertForDecision(httpBfDecisionCommunity)},
  437. decisions: []*models.Decision{httpBfDecisionCommunity, sshBfDecisionCommunity, sshBfDecisionCommunity, httpBfDecisionCommunity},
  438. },
  439. want: []*models.Alert{
  440. func() *models.Alert {
  441. a := createAlertForDecision(httpBfDecisionCommunity)
  442. a.Decisions = []*models.Decision{httpBfDecisionCommunity, sshBfDecisionCommunity, sshBfDecisionCommunity, httpBfDecisionCommunity}
  443. return a
  444. }(),
  445. },
  446. },
  447. {
  448. name: "List alert should pair up only with decisions having same scenario",
  449. args: args{
  450. alerts: []*models.Alert{createAlertForDecision(httpBfDecisionList), createAlertForDecision(sshBfDecisionList)},
  451. decisions: []*models.Decision{httpBfDecisionList, httpBfDecisionList, sshBfDecisionList, sshBfDecisionList},
  452. },
  453. want: []*models.Alert{
  454. func() *models.Alert {
  455. a := createAlertForDecision(httpBfDecisionList)
  456. a.Decisions = []*models.Decision{httpBfDecisionList, httpBfDecisionList}
  457. return a
  458. }(),
  459. func() *models.Alert {
  460. a := createAlertForDecision(sshBfDecisionList)
  461. a.Decisions = []*models.Decision{sshBfDecisionList, sshBfDecisionList}
  462. return a
  463. }(),
  464. },
  465. },
  466. }
  467. for _, tc := range tests {
  468. tc := tc
  469. t.Run(tc.name, func(t *testing.T) {
  470. addCounters, _ := makeAddAndDeleteCounters()
  471. if got := fillAlertsWithDecisions(tc.args.alerts, tc.args.decisions, addCounters); !reflect.DeepEqual(got, tc.want) {
  472. t.Errorf("fillAlertsWithDecisions() = %v, want %v", got, tc.want)
  473. }
  474. })
  475. }
  476. }
  477. func TestAPICPullTop(t *testing.T) {
  478. api := getAPIC(t)
  479. api.dbClient.Ent.Decision.Create().
  480. SetOrigin(types.CAPIOrigin).
  481. SetType("ban").
  482. SetValue("9.9.9.9").
  483. SetScope("Ip").
  484. SetScenario("crowdsecurity/ssh-bf").
  485. SetUntil(time.Now().Add(time.Hour)).
  486. ExecX(context.Background())
  487. assertTotalDecisionCount(t, api.dbClient, 1)
  488. assertTotalValidDecisionCount(t, api.dbClient, 1)
  489. httpmock.Activate()
  490. defer httpmock.DeactivateAndReset()
  491. httpmock.RegisterResponder("GET", "http://api.crowdsec.net/api/decisions/stream", httpmock.NewBytesResponder(
  492. 200, jsonMarshalX(
  493. modelscapi.GetDecisionsStreamResponse{
  494. Deleted: modelscapi.GetDecisionsStreamResponseDeleted{
  495. &modelscapi.GetDecisionsStreamResponseDeletedItem{
  496. Decisions: []string{
  497. "9.9.9.9", // This is already present in DB
  498. "9.1.9.9", // This not present in DB
  499. },
  500. Scope: types.StrPtr("Ip"),
  501. }, // This is already present in DB
  502. },
  503. New: modelscapi.GetDecisionsStreamResponseNew{
  504. &modelscapi.GetDecisionsStreamResponseNewItem{
  505. Scenario: types.StrPtr("crowdsecurity/test1"),
  506. Scope: types.StrPtr("Ip"),
  507. Decisions: []*modelscapi.GetDecisionsStreamResponseNewItemDecisionsItems0{
  508. {
  509. Value: types.StrPtr("1.2.3.4"),
  510. Duration: types.StrPtr("24h"),
  511. },
  512. },
  513. },
  514. &modelscapi.GetDecisionsStreamResponseNewItem{
  515. Scenario: types.StrPtr("crowdsecurity/test2"),
  516. Scope: types.StrPtr("Ip"),
  517. Decisions: []*modelscapi.GetDecisionsStreamResponseNewItemDecisionsItems0{
  518. {
  519. Value: types.StrPtr("1.2.3.5"),
  520. Duration: types.StrPtr("24h"),
  521. },
  522. },
  523. }, // These two are from community list.
  524. },
  525. Links: &modelscapi.GetDecisionsStreamResponseLinks{
  526. Blocklists: []*modelscapi.BlocklistLink{
  527. {
  528. URL: types.StrPtr("http://api.crowdsec.net/blocklist1"),
  529. Name: types.StrPtr("blocklist1"),
  530. Scope: types.StrPtr("Ip"),
  531. Remediation: types.StrPtr("ban"),
  532. Duration: types.StrPtr("24h"),
  533. },
  534. {
  535. URL: types.StrPtr("http://api.crowdsec.net/blocklist2"),
  536. Name: types.StrPtr("blocklist2"),
  537. Scope: types.StrPtr("Ip"),
  538. Remediation: types.StrPtr("ban"),
  539. Duration: types.StrPtr("24h"),
  540. },
  541. },
  542. },
  543. },
  544. ),
  545. ))
  546. httpmock.RegisterResponder("GET", "http://api.crowdsec.net/blocklist1", httpmock.NewStringResponder(
  547. 200, "1.2.3.6",
  548. ))
  549. httpmock.RegisterResponder("GET", "http://api.crowdsec.net/blocklist2", httpmock.NewStringResponder(
  550. 200, "1.2.3.7",
  551. ))
  552. url, err := url.ParseRequestURI("http://api.crowdsec.net/")
  553. require.NoError(t, err)
  554. apic, err := apiclient.NewDefaultClient(
  555. url,
  556. "/api",
  557. fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()),
  558. nil,
  559. )
  560. require.NoError(t, err)
  561. api.apiClient = apic
  562. err = api.PullTop()
  563. require.NoError(t, err)
  564. assertTotalDecisionCount(t, api.dbClient, 5)
  565. assertTotalValidDecisionCount(t, api.dbClient, 4)
  566. assertTotalAlertCount(t, api.dbClient, 3) // 2 for list sub , 1 for community list.
  567. alerts := api.dbClient.Ent.Alert.Query().AllX(context.Background())
  568. validDecisions := api.dbClient.Ent.Decision.Query().Where(
  569. decision.UntilGT(time.Now())).
  570. AllX(context.Background())
  571. decisionScenarioFreq := make(map[string]int)
  572. alertScenario := make(map[string]int)
  573. for _, alert := range alerts {
  574. alertScenario[alert.SourceScope]++
  575. }
  576. assert.Equal(t, 3, len(alertScenario))
  577. assert.Equal(t, 1, alertScenario[SCOPE_CAPI_ALIAS_ALIAS])
  578. assert.Equal(t, 1, alertScenario["lists:blocklist1"])
  579. assert.Equal(t, 1, alertScenario["lists:blocklist2"])
  580. for _, decisions := range validDecisions {
  581. decisionScenarioFreq[decisions.Scenario]++
  582. }
  583. assert.Equal(t, 1, decisionScenarioFreq["blocklist1"], 1)
  584. assert.Equal(t, 1, decisionScenarioFreq["blocklist2"], 1)
  585. assert.Equal(t, 1, decisionScenarioFreq["crowdsecurity/test1"], 1)
  586. assert.Equal(t, 1, decisionScenarioFreq["crowdsecurity/test2"], 1)
  587. }
  588. func TestAPICPullTopBLCacheFirstCall(t *testing.T) {
  589. // no decision in db, no last modified parameter.
  590. api := getAPIC(t)
  591. httpmock.Activate()
  592. defer httpmock.DeactivateAndReset()
  593. httpmock.RegisterResponder("GET", "http://api.crowdsec.net/api/decisions/stream", httpmock.NewBytesResponder(
  594. 200, jsonMarshalX(
  595. modelscapi.GetDecisionsStreamResponse{
  596. New: modelscapi.GetDecisionsStreamResponseNew{
  597. &modelscapi.GetDecisionsStreamResponseNewItem{
  598. Scenario: types.StrPtr("crowdsecurity/test1"),
  599. Scope: types.StrPtr("Ip"),
  600. Decisions: []*modelscapi.GetDecisionsStreamResponseNewItemDecisionsItems0{
  601. {
  602. Value: types.StrPtr("1.2.3.4"),
  603. Duration: types.StrPtr("24h"),
  604. },
  605. },
  606. },
  607. },
  608. Links: &modelscapi.GetDecisionsStreamResponseLinks{
  609. Blocklists: []*modelscapi.BlocklistLink{
  610. {
  611. URL: types.StrPtr("http://api.crowdsec.net/blocklist1"),
  612. Name: types.StrPtr("blocklist1"),
  613. Scope: types.StrPtr("Ip"),
  614. Remediation: types.StrPtr("ban"),
  615. Duration: types.StrPtr("24h"),
  616. },
  617. },
  618. },
  619. },
  620. ),
  621. ))
  622. httpmock.RegisterResponder("GET", "http://api.crowdsec.net/blocklist1", func(req *http.Request) (*http.Response, error) {
  623. assert.Equal(t, "", req.Header.Get("If-Modified-Since"))
  624. return httpmock.NewStringResponse(200, "1.2.3.4"), nil
  625. })
  626. url, err := url.ParseRequestURI("http://api.crowdsec.net/")
  627. require.NoError(t, err)
  628. apic, err := apiclient.NewDefaultClient(
  629. url,
  630. "/api",
  631. fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()),
  632. nil,
  633. )
  634. require.NoError(t, err)
  635. api.apiClient = apic
  636. err = api.PullTop()
  637. require.NoError(t, err)
  638. blocklistConfigItemName := fmt.Sprintf("blocklist:%s:last_pull", *types.StrPtr("blocklist1"))
  639. lastPullTimestamp, err := api.dbClient.GetConfigItem(blocklistConfigItemName)
  640. require.NoError(t, err)
  641. assert.NotEqual(t, "", *lastPullTimestamp)
  642. // new call should return 304 and should not change lastPullTimestamp
  643. httpmock.RegisterResponder("GET", "http://api.crowdsec.net/blocklist1", func(req *http.Request) (*http.Response, error) {
  644. assert.NotEqual(t, "", req.Header.Get("If-Modified-Since"))
  645. return httpmock.NewStringResponse(304, ""), nil
  646. })
  647. err = api.PullTop()
  648. require.NoError(t, err)
  649. secondLastPullTimestamp, err := api.dbClient.GetConfigItem(blocklistConfigItemName)
  650. require.NoError(t, err)
  651. assert.Equal(t, *lastPullTimestamp, *secondLastPullTimestamp)
  652. }
  653. func TestAPICPullTopBLCacheForceCall(t *testing.T) {
  654. api := getAPIC(t)
  655. httpmock.Activate()
  656. defer httpmock.DeactivateAndReset()
  657. // create a decision about to expire. It should force fetch
  658. alertInstance := api.dbClient.Ent.Alert.
  659. Create().
  660. SetScenario("update list").
  661. SetSourceScope("list:blocklist1").
  662. SetSourceValue("list:blocklist1").
  663. SaveX(context.Background())
  664. api.dbClient.Ent.Decision.Create().
  665. SetOrigin(types.ListOrigin).
  666. SetType("ban").
  667. SetValue("9.9.9.9").
  668. SetScope("Ip").
  669. SetScenario("blocklist1").
  670. SetUntil(time.Now().Add(time.Hour)).
  671. SetOwnerID(alertInstance.ID).
  672. ExecX(context.Background())
  673. httpmock.RegisterResponder("GET", "http://api.crowdsec.net/api/decisions/stream", httpmock.NewBytesResponder(
  674. 200, jsonMarshalX(
  675. modelscapi.GetDecisionsStreamResponse{
  676. New: modelscapi.GetDecisionsStreamResponseNew{
  677. &modelscapi.GetDecisionsStreamResponseNewItem{
  678. Scenario: types.StrPtr("crowdsecurity/test1"),
  679. Scope: types.StrPtr("Ip"),
  680. Decisions: []*modelscapi.GetDecisionsStreamResponseNewItemDecisionsItems0{
  681. {
  682. Value: types.StrPtr("1.2.3.4"),
  683. Duration: types.StrPtr("24h"),
  684. },
  685. },
  686. },
  687. },
  688. Links: &modelscapi.GetDecisionsStreamResponseLinks{
  689. Blocklists: []*modelscapi.BlocklistLink{
  690. {
  691. URL: types.StrPtr("http://api.crowdsec.net/blocklist1"),
  692. Name: types.StrPtr("blocklist1"),
  693. Scope: types.StrPtr("Ip"),
  694. Remediation: types.StrPtr("ban"),
  695. Duration: types.StrPtr("24h"),
  696. },
  697. },
  698. },
  699. },
  700. ),
  701. ))
  702. httpmock.RegisterResponder("GET", "http://api.crowdsec.net/blocklist1", func(req *http.Request) (*http.Response, error) {
  703. assert.Equal(t, "", req.Header.Get("If-Modified-Since"))
  704. return httpmock.NewStringResponse(304, ""), nil
  705. })
  706. url, err := url.ParseRequestURI("http://api.crowdsec.net/")
  707. require.NoError(t, err)
  708. apic, err := apiclient.NewDefaultClient(
  709. url,
  710. "/api",
  711. fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()),
  712. nil,
  713. )
  714. require.NoError(t, err)
  715. api.apiClient = apic
  716. err = api.PullTop()
  717. require.NoError(t, err)
  718. }
  719. func TestAPICPush(t *testing.T) {
  720. tests := []struct {
  721. name string
  722. alerts []*models.Alert
  723. expectedCalls int
  724. }{
  725. {
  726. name: "simple single alert",
  727. alerts: []*models.Alert{
  728. {
  729. Scenario: types.StrPtr("crowdsec/test"),
  730. ScenarioHash: types.StrPtr("certified"),
  731. ScenarioVersion: types.StrPtr("v1.0"),
  732. Simulated: types.BoolPtr(false),
  733. Source: &models.Source{},
  734. },
  735. },
  736. expectedCalls: 1,
  737. },
  738. {
  739. name: "simulated alert is not pushed",
  740. alerts: []*models.Alert{
  741. {
  742. Scenario: types.StrPtr("crowdsec/test"),
  743. ScenarioHash: types.StrPtr("certified"),
  744. ScenarioVersion: types.StrPtr("v1.0"),
  745. Simulated: types.BoolPtr(true),
  746. Source: &models.Source{},
  747. },
  748. },
  749. expectedCalls: 0,
  750. },
  751. {
  752. name: "1 request per 50 alerts",
  753. expectedCalls: 2,
  754. alerts: func() []*models.Alert {
  755. alerts := make([]*models.Alert, 100)
  756. for i := 0; i < 100; i++ {
  757. alerts[i] = &models.Alert{
  758. Scenario: types.StrPtr("crowdsec/test"),
  759. ScenarioHash: types.StrPtr("certified"),
  760. ScenarioVersion: types.StrPtr("v1.0"),
  761. Simulated: types.BoolPtr(false),
  762. Source: &models.Source{},
  763. }
  764. }
  765. return alerts
  766. }(),
  767. },
  768. }
  769. for _, tc := range tests {
  770. tc := tc
  771. t.Run(tc.name, func(t *testing.T) {
  772. api := getAPIC(t)
  773. api.pushInterval = time.Millisecond
  774. api.pushIntervalFirst = time.Millisecond
  775. url, err := url.ParseRequestURI("http://api.crowdsec.net/")
  776. require.NoError(t, err)
  777. httpmock.Activate()
  778. defer httpmock.DeactivateAndReset()
  779. apic, err := apiclient.NewDefaultClient(
  780. url,
  781. "/api",
  782. fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()),
  783. nil,
  784. )
  785. require.NoError(t, err)
  786. api.apiClient = apic
  787. httpmock.RegisterResponder("POST", "http://api.crowdsec.net/api/signals", httpmock.NewBytesResponder(200, []byte{}))
  788. go func() {
  789. api.AlertsAddChan <- tc.alerts
  790. time.Sleep(time.Second)
  791. api.Shutdown()
  792. }()
  793. err = api.Push()
  794. require.NoError(t, err)
  795. assert.Equal(t, tc.expectedCalls, httpmock.GetTotalCallCount())
  796. })
  797. }
  798. }
  799. func TestAPICSendMetrics(t *testing.T) {
  800. tests := []struct {
  801. name string
  802. duration time.Duration
  803. expectedCalls int
  804. setUp func(*apic)
  805. metricsInterval time.Duration
  806. }{
  807. {
  808. name: "basic",
  809. duration: time.Millisecond * 30,
  810. metricsInterval: time.Millisecond * 5,
  811. expectedCalls: 5,
  812. setUp: func(api *apic) {},
  813. },
  814. {
  815. name: "with some metrics",
  816. duration: time.Millisecond * 30,
  817. metricsInterval: time.Millisecond * 5,
  818. expectedCalls: 5,
  819. setUp: func(api *apic) {
  820. api.dbClient.Ent.Machine.Delete().ExecX(context.Background())
  821. api.dbClient.Ent.Machine.Create().
  822. SetMachineId("1234").
  823. SetPassword(testPassword.String()).
  824. SetIpAddress("1.2.3.4").
  825. SetScenarios("crowdsecurity/test").
  826. SetLastPush(time.Time{}).
  827. SetUpdatedAt(time.Time{}).
  828. ExecX(context.Background())
  829. api.dbClient.Ent.Bouncer.Delete().ExecX(context.Background())
  830. api.dbClient.Ent.Bouncer.Create().
  831. SetIPAddress("1.2.3.6").
  832. SetName("someBouncer").
  833. SetAPIKey("foobar").
  834. SetRevoked(false).
  835. SetLastPull(time.Time{}).
  836. ExecX(context.Background())
  837. },
  838. },
  839. }
  840. httpmock.RegisterResponder("POST", "http://api.crowdsec.net/api/metrics/", httpmock.NewBytesResponder(200, []byte{}))
  841. httpmock.Activate()
  842. defer httpmock.Deactivate()
  843. for _, tc := range tests {
  844. tc := tc
  845. t.Run(tc.name, func(t *testing.T) {
  846. url, err := url.ParseRequestURI("http://api.crowdsec.net/")
  847. require.NoError(t, err)
  848. apiClient, err := apiclient.NewDefaultClient(
  849. url,
  850. "/api",
  851. fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()),
  852. nil,
  853. )
  854. require.NoError(t, err)
  855. api := getAPIC(t)
  856. api.pushInterval = time.Millisecond
  857. api.pushIntervalFirst = time.Millisecond
  858. api.apiClient = apiClient
  859. api.metricsInterval = tc.metricsInterval
  860. api.metricsIntervalFirst = tc.metricsInterval
  861. tc.setUp(api)
  862. stop := make(chan bool)
  863. httpmock.ZeroCallCounters()
  864. go api.SendMetrics(stop)
  865. time.Sleep(tc.duration)
  866. stop <- true
  867. info := httpmock.GetCallCountInfo()
  868. noResponderCalls := info["NO_RESPONDER"]
  869. responderCalls := info["POST http://api.crowdsec.net/api/metrics/"]
  870. assert.LessOrEqual(t, absDiff(tc.expectedCalls, responderCalls), 2)
  871. assert.Zero(t, noResponderCalls)
  872. })
  873. }
  874. }
  875. func TestAPICPull(t *testing.T) {
  876. api := getAPIC(t)
  877. tests := []struct {
  878. name string
  879. setUp func()
  880. expectedDecisionCount int
  881. logContains string
  882. }{
  883. {
  884. name: "test pull if no scenarios are present",
  885. setUp: func() {},
  886. logContains: "scenario list is empty, will not pull yet",
  887. },
  888. {
  889. name: "test pull",
  890. setUp: func() {
  891. api.dbClient.Ent.Machine.Create().
  892. SetMachineId("1.2.3.4").
  893. SetPassword(testPassword.String()).
  894. SetIpAddress("1.2.3.4").
  895. SetScenarios("crowdsecurity/ssh-bf").
  896. ExecX(context.Background())
  897. },
  898. expectedDecisionCount: 1,
  899. },
  900. }
  901. for _, tc := range tests {
  902. tc := tc
  903. t.Run(tc.name, func(t *testing.T) {
  904. api = getAPIC(t)
  905. api.pullInterval = time.Millisecond
  906. api.pullIntervalFirst = time.Millisecond
  907. url, err := url.ParseRequestURI("http://api.crowdsec.net/")
  908. require.NoError(t, err)
  909. httpmock.Activate()
  910. defer httpmock.DeactivateAndReset()
  911. apic, err := apiclient.NewDefaultClient(
  912. url,
  913. "/api",
  914. fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()),
  915. nil,
  916. )
  917. require.NoError(t, err)
  918. api.apiClient = apic
  919. httpmock.RegisterNoResponder(httpmock.NewBytesResponder(200, jsonMarshalX(
  920. modelscapi.GetDecisionsStreamResponse{
  921. New: modelscapi.GetDecisionsStreamResponseNew{
  922. &modelscapi.GetDecisionsStreamResponseNewItem{
  923. Scenario: types.StrPtr("crowdsecurity/ssh-bf"),
  924. Scope: types.StrPtr("Ip"),
  925. Decisions: []*modelscapi.GetDecisionsStreamResponseNewItemDecisionsItems0{
  926. {
  927. Value: types.StrPtr("1.2.3.5"),
  928. Duration: types.StrPtr("24h"),
  929. },
  930. },
  931. },
  932. },
  933. },
  934. )))
  935. tc.setUp()
  936. var buf bytes.Buffer
  937. go func() {
  938. logrus.SetOutput(&buf)
  939. if err := api.Pull(); err != nil {
  940. panic(err)
  941. }
  942. }()
  943. //Slightly long because the CI runner for windows are slow, and this can lead to random failure
  944. time.Sleep(time.Millisecond * 500)
  945. logrus.SetOutput(os.Stderr)
  946. assert.Contains(t, buf.String(), tc.logContains)
  947. assertTotalDecisionCount(t, api.dbClient, tc.expectedDecisionCount)
  948. })
  949. }
  950. }
  951. func TestShouldShareAlert(t *testing.T) {
  952. tests := []struct {
  953. name string
  954. consoleConfig *csconfig.ConsoleConfig
  955. alert *models.Alert
  956. expectedRet bool
  957. expectedTrust string
  958. }{
  959. {
  960. name: "custom alert should be shared if config enables it",
  961. consoleConfig: &csconfig.ConsoleConfig{
  962. ShareCustomScenarios: types.BoolPtr(true),
  963. },
  964. alert: &models.Alert{Simulated: types.BoolPtr(false)},
  965. expectedRet: true,
  966. expectedTrust: "custom",
  967. },
  968. {
  969. name: "custom alert should not be shared if config disables it",
  970. consoleConfig: &csconfig.ConsoleConfig{
  971. ShareCustomScenarios: types.BoolPtr(false),
  972. },
  973. alert: &models.Alert{Simulated: types.BoolPtr(false)},
  974. expectedRet: false,
  975. expectedTrust: "custom",
  976. },
  977. {
  978. name: "manual alert should be shared if config enables it",
  979. consoleConfig: &csconfig.ConsoleConfig{
  980. ShareManualDecisions: types.BoolPtr(true),
  981. },
  982. alert: &models.Alert{
  983. Simulated: types.BoolPtr(false),
  984. Decisions: []*models.Decision{{Origin: types.StrPtr(types.CscliOrigin)}},
  985. },
  986. expectedRet: true,
  987. expectedTrust: "manual",
  988. },
  989. {
  990. name: "manual alert should not be shared if config disables it",
  991. consoleConfig: &csconfig.ConsoleConfig{
  992. ShareManualDecisions: types.BoolPtr(false),
  993. },
  994. alert: &models.Alert{
  995. Simulated: types.BoolPtr(false),
  996. Decisions: []*models.Decision{{Origin: types.StrPtr(types.CscliOrigin)}},
  997. },
  998. expectedRet: false,
  999. expectedTrust: "manual",
  1000. },
  1001. {
  1002. name: "manual alert should be shared if config enables it",
  1003. consoleConfig: &csconfig.ConsoleConfig{
  1004. ShareTaintedScenarios: types.BoolPtr(true),
  1005. },
  1006. alert: &models.Alert{
  1007. Simulated: types.BoolPtr(false),
  1008. ScenarioHash: types.StrPtr("whateverHash"),
  1009. },
  1010. expectedRet: true,
  1011. expectedTrust: "tainted",
  1012. },
  1013. {
  1014. name: "manual alert should not be shared if config disables it",
  1015. consoleConfig: &csconfig.ConsoleConfig{
  1016. ShareTaintedScenarios: types.BoolPtr(false),
  1017. },
  1018. alert: &models.Alert{
  1019. Simulated: types.BoolPtr(false),
  1020. ScenarioHash: types.StrPtr("whateverHash"),
  1021. },
  1022. expectedRet: false,
  1023. expectedTrust: "tainted",
  1024. },
  1025. }
  1026. for _, tc := range tests {
  1027. tc := tc
  1028. t.Run(tc.name, func(t *testing.T) {
  1029. ret := shouldShareAlert(tc.alert, tc.consoleConfig)
  1030. assert.Equal(t, tc.expectedRet, ret)
  1031. })
  1032. }
  1033. }