crowdsec/pkg/apiserver/apiserver_test.go
2022-11-29 09:16:07 +01:00

444 lines
12 KiB
Go

package apiserver
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
middlewares "github.com/crowdsecurity/crowdsec/pkg/apiserver/middlewares/v1"
"github.com/crowdsecurity/crowdsec/pkg/cwversion"
"github.com/crowdsecurity/crowdsec/pkg/models"
"github.com/crowdsecurity/crowdsec/pkg/types"
"github.com/go-openapi/strfmt"
"github.com/pkg/errors"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/database"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
var testMachineID = "test"
var testPassword = strfmt.Password("test")
var MachineTest = models.WatcherAuthRequest{
MachineID: &testMachineID,
Password: &testPassword,
}
var UserAgent = fmt.Sprintf("crowdsec-test/%s", cwversion.Version)
var emptyBody = strings.NewReader("")
func LoadTestConfig() csconfig.Config {
config := csconfig.Config{}
maxAge := "1h"
flushConfig := csconfig.FlushDBCfg{
MaxAge: &maxAge,
}
tempDir, _ := os.MkdirTemp("", "crowdsec_tests")
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
if err := config.API.Server.LoadProfiles(); err != nil {
log.Fatalf("failed to load profiles: %s", err)
}
return config
}
func LoadTestConfigForwardedFor() csconfig.Config {
config := csconfig.Config{}
maxAge := "1h"
flushConfig := csconfig.FlushDBCfg{
MaxAge: &maxAge,
}
tempDir, _ := os.MkdirTemp("", "crowdsec_tests")
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
if err := config.API.Server.LoadProfiles(); err != nil {
log.Fatalf("failed to load profiles: %s", err)
}
return config
}
func NewAPIServer() (*APIServer, csconfig.Config, error) {
config := LoadTestConfig()
os.Remove("./ent")
apiServer, err := NewServer(config.API.Server)
if err != nil {
return nil, config, fmt.Errorf("unable to run local API: %s", err)
}
log.Printf("Creating new API server")
gin.SetMode(gin.TestMode)
return apiServer, config, nil
}
func NewAPITest() (*gin.Engine, csconfig.Config, error) {
apiServer, config, err := NewAPIServer()
if err != nil {
return nil, config, fmt.Errorf("unable to run local API: %s", err)
}
err = apiServer.InitController()
if err != nil {
return nil, config, fmt.Errorf("unable to run local API: %s", err)
}
router, err := apiServer.Router()
if err != nil {
return nil, config, fmt.Errorf("unable to run local API: %s", err)
}
return router, config, nil
}
func NewAPITestForwardedFor() (*gin.Engine, csconfig.Config, error) {
config := LoadTestConfigForwardedFor()
os.Remove("./ent")
apiServer, err := NewServer(config.API.Server)
if err != nil {
return nil, config, fmt.Errorf("unable to run local API: %s", err)
}
err = apiServer.InitController()
if err != nil {
return nil, config, fmt.Errorf("unable to run local API: %s", err)
}
log.Printf("Creating new API server")
gin.SetMode(gin.TestMode)
router, err := apiServer.Router()
if err != nil {
return nil, config, fmt.Errorf("unable to run local API: %s", err)
}
return router, config, nil
}
func ValidateMachine(machineID string, config *csconfig.DatabaseCfg) error {
dbClient, err := database.NewClient(config)
if err != nil {
return fmt.Errorf("unable to create new database client: %s", err)
}
if err := dbClient.ValidateMachine(machineID); err != nil {
return fmt.Errorf("unable to validate machine: %s", err)
}
return nil
}
func GetMachineIP(machineID string, config *csconfig.DatabaseCfg) (string, error) {
dbClient, err := database.NewClient(config)
if err != nil {
return "", fmt.Errorf("unable to create new database client: %s", err)
}
machines, err := dbClient.ListMachines()
if err != nil {
return "", fmt.Errorf("Unable to list machines: %s", err)
}
for _, machine := range machines {
if machine.MachineId == machineID {
return machine.IpAddress, nil
}
}
return "", nil
}
func GetAlertReaderFromFile(path string) *strings.Reader {
alertContentBytes, err := os.ReadFile(path)
if err != nil {
log.Fatal(err)
}
alerts := make([]*models.Alert, 0)
if err := json.Unmarshal(alertContentBytes, &alerts); err != nil {
log.Fatal(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)
if err != nil {
log.Fatal(err)
}
return strings.NewReader(string(alertContent))
}
func readDecisionsGetResp(resp *httptest.ResponseRecorder) ([]*models.Decision, int, error) {
var response []*models.Decision
if resp == nil {
return nil, 0, errors.New("response is nil")
}
err := json.Unmarshal(resp.Body.Bytes(), &response)
if err != nil {
return nil, resp.Code, err
}
return response, resp.Code, nil
}
func readDecisionsErrorResp(resp *httptest.ResponseRecorder) (map[string]string, int, error) {
var response map[string]string
if resp == nil {
return nil, 0, errors.New("response is nil")
}
err := json.Unmarshal(resp.Body.Bytes(), &response)
if err != nil {
return nil, resp.Code, err
}
return response, resp.Code, nil
}
func readDecisionsDeleteResp(resp *httptest.ResponseRecorder) (*models.DeleteDecisionResponse, int, error) {
var response models.DeleteDecisionResponse
if resp == nil {
return nil, 0, errors.New("response is nil")
}
err := json.Unmarshal(resp.Body.Bytes(), &response)
if err != nil {
return nil, resp.Code, err
}
return &response, resp.Code, nil
}
func readDecisionsStreamResp(resp *httptest.ResponseRecorder) (map[string][]*models.Decision, int, error) {
response := make(map[string][]*models.Decision)
if resp == nil {
return nil, 0, errors.New("response is nil")
}
err := json.Unmarshal(resp.Body.Bytes(), &response)
if err != nil {
return nil, resp.Code, err
}
return response, resp.Code, nil
}
func CreateTestMachine(router *gin.Engine) (string, error) {
b, err := json.Marshal(MachineTest)
if err != nil {
return "", fmt.Errorf("unable to marshal MachineTest")
}
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, nil
}
func CreateTestBouncer(config *csconfig.DatabaseCfg) (string, error) {
dbClient, err := database.NewClient(config)
if err != nil {
log.Fatalf("unable to create new database client: %s", err)
}
apiKey, err := middlewares.GenerateAPIKey(keyLength)
if err != nil {
return "", fmt.Errorf("unable to generate api key: %s", err)
}
_, err = dbClient.CreateBouncer("test", "127.0.0.1", middlewares.HashSHA512(apiKey), types.ApiKeyAuthType)
if err != nil {
return "", fmt.Errorf("unable to create blocker: %s", err)
}
return apiKey, nil
}
func TestWithWrongDBConfig(t *testing.T) {
config := LoadTestConfig()
config.API.Server.DbConfig.Type = "test"
apiServer, err := NewServer(config.API.Server)
assert.Equal(t, apiServer, &APIServer{})
assert.Equal(t, "unable to init database client: unknown database type 'test'", err.Error())
}
func TestWithWrongFlushConfig(t *testing.T) {
config := LoadTestConfig()
maxItems := -1
config.API.Server.DbConfig.Flush.MaxItems = &maxItems
apiServer, err := NewServer(config.API.Server)
assert.Equal(t, apiServer, &APIServer{})
assert.Equal(t, "max_items can't be zero or negative number", err.Error())
}
func TestUnknownPath(t *testing.T) {
router, _, err := NewAPITest()
if err != nil {
log.Fatalf("unable to run local API: %s", err)
}
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")
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,
}
lvl := log.DebugLevel
expectedFile := fmt.Sprintf("%s/crowdsec_api.log", tempDir)
expectedLines := []string{"/test42"}
cfg.LogLevel = &lvl
// Configure logging
if err := types.SetDefaultLoggerConfig(cfg.LogMedia, cfg.LogDir, *cfg.LogLevel, cfg.LogMaxSize, cfg.LogMaxFiles, cfg.LogMaxAge, cfg.CompressLogs, false); err != nil {
t.Fatal(err)
}
api, err := NewServer(&cfg)
if err != nil {
t.Fatalf("failed to create api : %s", err)
}
if api == nil {
t.Fatalf("failed to create api #2 is nbill")
}
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)
if err != nil {
t.Fatalf("failed to read file : %s", err)
}
for _, expectedStr := range expectedLines {
if !strings.Contains(string(data), expectedStr) {
t.Fatalf("expected %s in %s", expectedStr, string(data))
}
}
}
func TestLoggingErrorToFileConfig(t *testing.T) {
/*declare settings*/
maxAge := "1h"
flushConfig := csconfig.FlushDBCfg{
MaxAge: &maxAge,
}
tempDir, _ := os.MkdirTemp("", "crowdsec_tests")
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,
}
lvl := log.ErrorLevel
expectedFile := fmt.Sprintf("%s/crowdsec_api.log", tempDir)
cfg.LogLevel = &lvl
// Configure logging
if err := types.SetDefaultLoggerConfig(cfg.LogMedia, cfg.LogDir, *cfg.LogLevel, cfg.LogMaxSize, cfg.LogMaxFiles, cfg.LogMaxAge, cfg.CompressLogs, false); err != nil {
t.Fatal(err)
}
api, err := NewServer(&cfg)
if err != nil {
t.Fatalf("failed to create api : %s", err)
}
if api == nil {
t.Fatalf("failed to create api #2 is nbill")
}
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
x, err := os.ReadFile(expectedFile)
if err == nil && len(x) > 0 {
t.Fatalf("file should be empty, got '%s'", x)
}
os.Remove("./crowdsec.log")
os.Remove(expectedFile)
}