Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add webhook:call actionner #356

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions actionners/actionners.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
}
}
}
Expand Down
144 changes: 144 additions & 0 deletions actionners/webhook/call.go
Original file line number Diff line number Diff line change
@@ -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", "")
}
}
}
56 changes: 56 additions & 0 deletions internal/http/client.go
Original file line number Diff line number Diff line change
@@ -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
}
}
4 changes: 4 additions & 0 deletions utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
Loading