From 31f0539594756c4dbd5990c2a9fdcb6d86839d58 Mon Sep 17 00:00:00 2001 From: Igor Eulalio Date: Tue, 2 Jul 2024 14:15:23 -0300 Subject: [PATCH] add webhook:call actionner --- actionners/actionners.go | 16 ++++- actionners/webhook/call.go | 144 +++++++++++++++++++++++++++++++++++++ internal/http/client.go | 56 +++++++++++++++ utils/utils.go | 4 ++ 4 files changed, 217 insertions(+), 3 deletions(-) create mode 100644 actionners/webhook/call.go create mode 100644 internal/http/client.go diff --git a/actionners/actionners.go b/actionners/actionners.go index dedaf8c6..b118b98a 100644 --- a/actionners/actionners.go +++ b/actionners/actionners.go @@ -5,6 +5,8 @@ import ( "fmt" lambdaInvoke "github.com/falco-talon/falco-talon/actionners/aws/lambda" + "github.com/falco-talon/falco-talon/actionners/webhook" + "github.com/falco-talon/falco-talon/internal/http" "github.com/falco-talon/falco-talon/outputs" calicoNetworkpolicy "github.com/falco-talon/falco-talon/actionners/calico/networkpolicy" @@ -196,6 +198,15 @@ func GetDefaultActionners() *Actionners { Action: k8sTcpdump.Action, RequireOutput: true, }, + &Actionner{ + Category: "webhook", + Name: "call", + DefaultContinue: true, + Init: http.Init, + Checks: []checkActionner{}, + CheckParameters: webhook.CheckParameters, + Action: webhook.Action, + }, &Actionner{ Category: "aws", Name: "lambda", @@ -252,17 +263,16 @@ func Init() error { } for category := range categories { + utils.PrintLog("info", utils.LogLine{Message: "init", ActionnerCategory: category}) for _, actionner := range *availableActionners { if category == actionner.Category { if actionner.Init != nil { - utils.PrintLog("info", utils.LogLine{Message: "init", ActionnerCategory: actionner.Category}) if err := actionner.Init(); err != nil { utils.PrintLog("error", utils.LogLine{Message: "init", Error: err.Error(), ActionnerCategory: actionner.Category}) return err } - enabledCategories[category] = true } - break // we break to avoid to repeat the same init() several times + enabledCategories[category] = true } } } diff --git a/actionners/webhook/call.go b/actionners/webhook/call.go new file mode 100644 index 00000000..1d4616da --- /dev/null +++ b/actionners/webhook/call.go @@ -0,0 +1,144 @@ +package webhook + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + + "github.com/go-playground/validator/v10" + + "github.com/falco-talon/falco-talon/internal/events" + httpClient "github.com/falco-talon/falco-talon/internal/http" + "github.com/falco-talon/falco-talon/internal/rules" + "github.com/falco-talon/falco-talon/outputs/model" + "github.com/falco-talon/falco-talon/utils" +) + +type AuthConfig struct { + Username string `mapstructure:"username" validate:"required_with=Password,omitempty"` + Password string `mapstructure:"password" validate:"required_with=Username,omitempty"` + Token string `mapstructure:"token" validate:"omitempty"` +} + +type HTTPConfig struct { + Headers map[string]string `mapstructure:"headers" validate:"omitempty"` + Method string `mapstructure:"method" validate:"omitempty,oneof=GET POST PUT DELETE PATCH"` + Timeout int `mapstructure:"timeout" validate:"omitempty"` +} + +type Config struct { + Auth AuthConfig `mapstructure:"auth_config" validate:"omitempty"` + HTTPConfig *HTTPConfig `mapstructure:"http_config" validate:"omitempty"` + Endpoint string `mapstructure:"endpoint" validate:"required,url"` + Port int `mapstructure:"port" validate:"required"` + InsecureSkipTLS bool `mapstructure:"insecure_skip_tls" validate:"omitempty"` +} + +func Action(action *rules.Action, event *events.Event) (utils.LogLine, *model.Data, error) { + var actionConfig Config + err := utils.DecodeParams(action.GetParameters(), &actionConfig) + if err != nil { + return utils.LogLine{ + Objects: nil, + Error: err.Error(), + Status: "failure", + }, + nil, + err + } + + client := httpClient.GetClient() + + httpClient.OverrideClientSettings(client, actionConfig.InsecureSkipTLS, actionConfig.HTTPConfig.Timeout) + + err = callHTTP(client, actionConfig, event) + if err != nil { + return utils.LogLine{ + Error: err.Error(), + Status: "failure", + }, + nil, + err + } + + return utils.LogLine{ + Message: "successfully called webhook", + Status: "success", + }, nil, nil +} + +func CheckParameters(action *rules.Action) error { + parameters := action.GetParameters() + + var config Config + + err := utils.DecodeParams(parameters, &config) + if err != nil { + return err + } + + utils.AddCustomStructValidation(AuthConfig{}, authConfigStructLevelValidation) + + err = utils.ValidateStruct(config) + if err != nil { + return err + } + + return nil +} + +func callHTTP(client *http.Client, config Config, event *events.Event) error { + payload, err := json.Marshal(event) + if err != nil { + return err + } + + method := http.MethodPost + if config.HTTPConfig.Method != "" { + method = config.HTTPConfig.Method + } + + url := fmt.Sprintf("%s:%d", config.Endpoint, config.Port) + req, err := http.NewRequest(method, url, bytes.NewBuffer(payload)) + if err != nil { + return err + } + + for key, value := range config.HTTPConfig.Headers { + req.Header.Set(key, value) + } + + if config.Auth.Token != "" { + req.Header.Set("Authorization", "Bearer "+config.Auth.Token) + } else if config.Auth.Username != "" && config.Auth.Password != "" { + req.SetBasicAuth(config.Auth.Username, config.Auth.Password) + } + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("received non-2xx response status: %d", resp.StatusCode) + } + + return nil +} + +func authConfigStructLevelValidation(sl validator.StructLevel) { + authConfig := sl.Current().Interface().(AuthConfig) + + if authConfig.Token != "" { + if authConfig.Username != "" || authConfig.Password != "" { + sl.ReportError(authConfig.Token, "Token", "Token", "auth_config requires either token or username and password.", "") + } + } else { + if authConfig.Username == "" || authConfig.Password == "" { + sl.ReportError(authConfig.Username, "Username", "Username", "auth_config accepts only token or username and password", "") + sl.ReportError(authConfig.Password, "Password", "Password", "auth_config accepts only token or username and password", "") + } + } +} diff --git a/internal/http/client.go b/internal/http/client.go new file mode 100644 index 00000000..9ef63126 --- /dev/null +++ b/internal/http/client.go @@ -0,0 +1,56 @@ +package http + +import ( + "crypto/tls" + "net/http" + "sync" + "time" +) + +var httpClient *http.Client +var once sync.Once + +const ( + DefaultTimeout = 10 + InsecureSkipVerify = false +) + +func Init() error { + if httpClient != nil { + return nil + } + + once.Do(func() { + timeout := time.Duration(DefaultTimeout) * time.Second // Default timeout + httpClient = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: InsecureSkipVerify}, + }, + Timeout: timeout, + } + }) + return nil +} + +func GetClient() *http.Client { + return httpClient +} + +func OverrideClientSettings(client *http.Client, skipTLS bool, timeout int) { + if timeout > 0 { + client.Timeout = time.Duration(timeout) * time.Second + } + + if skipTLS { + updateInsecureSkipVerify(client, skipTLS) + } +} + +func updateInsecureSkipVerify(client *http.Client, skipVerify bool) { + if client != nil && client.Transport != nil { + oldTransport := client.Transport.(*http.Transport) + newTransport := oldTransport.Clone() + newTransport.TLSClientConfig.InsecureSkipVerify = skipVerify + client.Transport = newTransport + } +} diff --git a/utils/utils.go b/utils/utils.go index b15cb3cf..752e5df7 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -241,6 +241,10 @@ func AddCustomValidation(tag string, fn validator.Func) error { return nil } +func AddCustomStructValidation(s interface{}, fn validator.StructLevelFunc) { + validate.RegisterStructValidation(fn, s) +} + func DecodeParams(params map[string]interface{}, result interface{}) error { // Decode parameters into the struct decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{