package apiserver import ( "encoding/json" "fmt" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "time" "github.com/gin-gonic/gin" "github.com/go-openapi/strfmt" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/crowdsecurity/go-cs-lib/cstest" "github.com/crowdsecurity/go-cs-lib/ptr" "github.com/crowdsecurity/go-cs-lib/version" middlewares "github.com/crowdsecurity/crowdsec/pkg/apiserver/middlewares/v1" "github.com/crowdsecurity/crowdsec/pkg/csconfig" "github.com/crowdsecurity/crowdsec/pkg/database" "github.com/crowdsecurity/crowdsec/pkg/models" "github.com/crowdsecurity/crowdsec/pkg/types" ) var testMachineID = "test" var testPassword = strfmt.Password("test") var MachineTest = models.WatcherAuthRequest{ MachineID: &testMachineID, Password: &testPassword, } var UserAgent = fmt.Sprintf("crowdsec-test/%s", version.Version) var emptyBody = strings.NewReader("") func LoadTestConfig(t *testing.T) csconfig.Config { config := csconfig.Config{} maxAge := "1h" flushConfig := csconfig.FlushDBCfg{ MaxAge: &maxAge, } tempDir, _ := os.MkdirTemp("", "crowdsec_tests") t.Cleanup(func() { os.RemoveAll(tempDir) }) dbconfig := csconfig.DatabaseCfg{ Type: "sqlite", DbPath: filepath.Join(tempDir, "ent"), Flush: &flushConfig, } apiServerConfig := csconfig.LocalApiServerCfg{ ListenURI: "http://127.0.0.1:8080", DbConfig: &dbconfig, ProfilesPath: "./tests/profiles.yaml", ConsoleConfig: &csconfig.ConsoleConfig{ ShareManualDecisions: new(bool), ShareTaintedScenarios: new(bool), ShareCustomScenarios: new(bool), }, } apiConfig := csconfig.APICfg{ Server: &apiServerConfig, } config.API = &apiConfig err := config.API.Server.LoadProfiles() require.NoError(t, err) return config } func LoadTestConfigForwardedFor(t *testing.T) csconfig.Config { config := csconfig.Config{} maxAge := "1h" flushConfig := csconfig.FlushDBCfg{ MaxAge: &maxAge, } tempDir, _ := os.MkdirTemp("", "crowdsec_tests") t.Cleanup(func() { os.RemoveAll(tempDir) }) dbconfig := csconfig.DatabaseCfg{ Type: "sqlite", DbPath: filepath.Join(tempDir, "ent"), Flush: &flushConfig, } apiServerConfig := csconfig.LocalApiServerCfg{ ListenURI: "http://127.0.0.1:8080", DbConfig: &dbconfig, ProfilesPath: "./tests/profiles.yaml", UseForwardedForHeaders: true, TrustedProxies: &[]string{"0.0.0.0/0"}, ConsoleConfig: &csconfig.ConsoleConfig{ ShareManualDecisions: new(bool), ShareTaintedScenarios: new(bool), ShareCustomScenarios: new(bool), }, } apiConfig := csconfig.APICfg{ Server: &apiServerConfig, } config.API = &apiConfig err := config.API.Server.LoadProfiles() require.NoError(t, err) return config } func NewAPIServer(t *testing.T) (*APIServer, csconfig.Config) { config := LoadTestConfig(t) os.Remove("./ent") apiServer, err := NewServer(config.API.Server) require.NoError(t, err) log.Printf("Creating new API server") gin.SetMode(gin.TestMode) return apiServer, config } func NewAPITest(t *testing.T) (*gin.Engine, csconfig.Config) { apiServer, config := NewAPIServer(t) err := apiServer.InitController() require.NoError(t, err) router, err := apiServer.Router() require.NoError(t, err) return router, config } func NewAPITestForwardedFor(t *testing.T) (*gin.Engine, csconfig.Config) { config := LoadTestConfigForwardedFor(t) os.Remove("./ent") apiServer, err := NewServer(config.API.Server) require.NoError(t, err) err = apiServer.InitController() require.NoError(t, err) log.Printf("Creating new API server") gin.SetMode(gin.TestMode) router, err := apiServer.Router() require.NoError(t, err) return router, config } func ValidateMachine(t *testing.T, machineID string, config *csconfig.DatabaseCfg) { dbClient, err := database.NewClient(config) require.NoError(t, err) err = dbClient.ValidateMachine(machineID) require.NoError(t, err) } func GetMachineIP(t *testing.T, machineID string, config *csconfig.DatabaseCfg) string { dbClient, err := database.NewClient(config) require.NoError(t, err) machines, err := dbClient.ListMachines() require.NoError(t, err) for _, machine := range machines { if machine.MachineId == machineID { return machine.IpAddress } } return "" } func GetAlertReaderFromFile(t *testing.T, path string) *strings.Reader { alertContentBytes, err := os.ReadFile(path) require.NoError(t, err) alerts := make([]*models.Alert, 0) err = json.Unmarshal(alertContentBytes, &alerts) require.NoError(t, err) for _, alert := range alerts { *alert.StartAt = time.Now().UTC().Format(time.RFC3339) *alert.StopAt = time.Now().UTC().Format(time.RFC3339) } alertContent, err := json.Marshal(alerts) require.NoError(t, err) return strings.NewReader(string(alertContent)) } func readDecisionsGetResp(t *testing.T, resp *httptest.ResponseRecorder) ([]*models.Decision, int) { var response []*models.Decision require.NotNil(t, resp) err := json.Unmarshal(resp.Body.Bytes(), &response) require.NoError(t, err) return response, resp.Code } func readDecisionsErrorResp(t *testing.T, resp *httptest.ResponseRecorder) (map[string]string, int) { var response map[string]string require.NotNil(t, resp) err := json.Unmarshal(resp.Body.Bytes(), &response) require.NoError(t, err) return response, resp.Code } func readDecisionsDeleteResp(t *testing.T, resp *httptest.ResponseRecorder) (*models.DeleteDecisionResponse, int) { var response models.DeleteDecisionResponse require.NotNil(t, resp) err := json.Unmarshal(resp.Body.Bytes(), &response) require.NoError(t, err) return &response, resp.Code } func readDecisionsStreamResp(t *testing.T, resp *httptest.ResponseRecorder) (map[string][]*models.Decision, int) { response := make(map[string][]*models.Decision) require.NotNil(t, resp) err := json.Unmarshal(resp.Body.Bytes(), &response) require.NoError(t, err) return response, resp.Code } func CreateTestMachine(t *testing.T, router *gin.Engine) string { b, err := json.Marshal(MachineTest) require.NoError(t, err) body := string(b) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodPost, "/v1/watchers", strings.NewReader(body)) req.Header.Set("User-Agent", UserAgent) router.ServeHTTP(w, req) return body } func CreateTestBouncer(t *testing.T, config *csconfig.DatabaseCfg) string { dbClient, err := database.NewClient(config) require.NoError(t, err) apiKey, err := middlewares.GenerateAPIKey(keyLength) require.NoError(t, err) _, err = dbClient.CreateBouncer("test", "127.0.0.1", middlewares.HashSHA512(apiKey), types.ApiKeyAuthType) require.NoError(t, err) return apiKey } func TestWithWrongDBConfig(t *testing.T) { config := LoadTestConfig(t) config.API.Server.DbConfig.Type = "test" apiServer, err := NewServer(config.API.Server) cstest.RequireErrorContains(t, err, "unable to init database client: unknown database type 'test'") assert.Nil(t, apiServer) } func TestWithWrongFlushConfig(t *testing.T) { config := LoadTestConfig(t) maxItems := -1 config.API.Server.DbConfig.Flush.MaxItems = &maxItems apiServer, err := NewServer(config.API.Server) cstest.RequireErrorContains(t, err, "max_items can't be zero or negative number") assert.Nil(t, apiServer) } func TestUnknownPath(t *testing.T) { router, _ := NewAPITest(t) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/test", nil) req.Header.Set("User-Agent", UserAgent) router.ServeHTTP(w, req) assert.Equal(t, 404, w.Code) } /* ListenURI string `yaml:"listen_uri,omitempty"` //127.0.0.1:8080 TLS *TLSCfg `yaml:"tls"` DbConfig *DatabaseCfg `yaml:"-"` LogDir string `yaml:"-"` LogMedia string `yaml:"-"` OnlineClient *OnlineApiClientCfg `yaml:"online_client"` ProfilesPath string `yaml:"profiles_path,omitempty"` Profiles []*ProfileCfg `yaml:"-"` LogLevel *log.Level `yaml:"log_level"` UseForwardedForHeaders bool `yaml:"use_forwarded_for_headers,omitempty"` */ func TestLoggingDebugToFileConfig(t *testing.T) { /*declare settings*/ maxAge := "1h" flushConfig := csconfig.FlushDBCfg{ MaxAge: &maxAge, } tempDir, _ := os.MkdirTemp("", "crowdsec_tests") t.Cleanup(func() { os.RemoveAll(tempDir) }) dbconfig := csconfig.DatabaseCfg{ Type: "sqlite", DbPath: filepath.Join(tempDir, "ent"), Flush: &flushConfig, } cfg := csconfig.LocalApiServerCfg{ ListenURI: "127.0.0.1:8080", LogMedia: "file", LogDir: tempDir, DbConfig: &dbconfig, } expectedFile := fmt.Sprintf("%s/crowdsec_api.log", tempDir) expectedLines := []string{"/test42"} cfg.LogLevel = ptr.Of(log.DebugLevel) // Configure logging err := types.SetDefaultLoggerConfig(cfg.LogMedia, cfg.LogDir, *cfg.LogLevel, cfg.LogMaxSize, cfg.LogMaxFiles, cfg.LogMaxAge, cfg.CompressLogs, false) require.NoError(t, err) api, err := NewServer(&cfg) require.NoError(t, err) require.NotNil(t, api) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/test42", nil) req.Header.Set("User-Agent", UserAgent) api.router.ServeHTTP(w, req) assert.Equal(t, 404, w.Code) //wait for the request to happen time.Sleep(500 * time.Millisecond) //check file content data, err := os.ReadFile(expectedFile) require.NoError(t, err) for _, expectedStr := range expectedLines { assert.Contains(t, string(data), expectedStr) } } func TestLoggingErrorToFileConfig(t *testing.T) { /*declare settings*/ maxAge := "1h" flushConfig := csconfig.FlushDBCfg{ MaxAge: &maxAge, } tempDir, _ := os.MkdirTemp("", "crowdsec_tests") t.Cleanup(func() { os.RemoveAll(tempDir) }) dbconfig := csconfig.DatabaseCfg{ Type: "sqlite", DbPath: filepath.Join(tempDir, "ent"), Flush: &flushConfig, } cfg := csconfig.LocalApiServerCfg{ ListenURI: "127.0.0.1:8080", LogMedia: "file", LogDir: tempDir, DbConfig: &dbconfig, } expectedFile := fmt.Sprintf("%s/crowdsec_api.log", tempDir) cfg.LogLevel = ptr.Of(log.ErrorLevel) // Configure logging err := types.SetDefaultLoggerConfig(cfg.LogMedia, cfg.LogDir, *cfg.LogLevel, cfg.LogMaxSize, cfg.LogMaxFiles, cfg.LogMaxAge, cfg.CompressLogs, false) require.NoError(t, err) api, err := NewServer(&cfg) require.NoError(t, err) require.NotNil(t, api) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/test42", nil) req.Header.Set("User-Agent", UserAgent) api.router.ServeHTTP(w, req) assert.Equal(t, http.StatusNotFound, w.Code) //wait for the request to happen time.Sleep(500 * time.Millisecond) //check file content x, err := os.ReadFile(expectedFile) if err == nil { require.Empty(t, x) } os.Remove("./crowdsec.log") os.Remove(expectedFile) }