apic_test.go 25 KB

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