apic_test.go 25 KB

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