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

v0.2.0 #487

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ DO-NOT-COMMIT-local-setup.yaml
deployment/compose/minio/data
deployment/compose/tempo/data
deployment/compose/loki/data
.env
.env
pod*.yaml
2 changes: 2 additions & 0 deletions actionners/actionners.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
lambdaInvoke "github.com/falcosecurity/falco-talon/actionners/aws/lambda"
calicoNetworkpolicy "github.com/falcosecurity/falco-talon/actionners/calico/networkpolicy"
ciliumNetworkpolicy "github.com/falcosecurity/falco-talon/actionners/cilium/networkpolicy"
gcpFunctionCall "github.com/falcosecurity/falco-talon/actionners/gcp/function"
k8sCordon "github.com/falcosecurity/falco-talon/actionners/kubernetes/cordon"
k8sDelete "github.com/falcosecurity/falco-talon/actionners/kubernetes/delete"
k8sDownload "github.com/falcosecurity/falco-talon/actionners/kubernetes/download"
Expand Down Expand Up @@ -81,6 +82,7 @@ func ListDefaultActionners() *Actionners {
k8sDownload.Register(),
k8sTcpdump.Register(),
lambdaInvoke.Register(),
gcpFunctionCall.Register(),
calicoNetworkpolicy.Register(),
ciliumNetworkpolicy.Register(),
)
Expand Down
255 changes: 255 additions & 0 deletions actionners/gcp/function/function.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
package functions

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"time"

"cloud.google.com/go/functions/apiv2/functionspb"
"google.golang.org/api/idtoken"

"github.com/falcosecurity/falco-talon/internal/events"
"github.com/falcosecurity/falco-talon/internal/gcp/checks"
"github.com/falcosecurity/falco-talon/internal/gcp/client"
"github.com/falcosecurity/falco-talon/internal/models"
"github.com/falcosecurity/falco-talon/internal/rules"
"github.com/falcosecurity/falco-talon/utils"
)

const (
Name string = "function"
Category string = "gcp"
Description string = "Invoke a GCP function forwarding the Falco event payload"
Source string = "any"
Continue bool = true
AllowOutput bool = false
RequireOutput bool = false
Permissions string = `{
"cloudfunctions.functions.get",
"cloudfunctions.functions.invoke"
}`
Example string = `- action: Invoke GCP Cloud Function
actionner: gcp:function
parameters:
gcp_function_name: sample-function
gcp_function_location: us-central1
gcp_function_timeout: 10
`
)

var (
RequiredOutputFields = []string{}
)

type Parameters struct {
GCPFunctionName string `mapstructure:"gcp_function_name" validate:"required"`
GCPFunctionLocation string `mapstructure:"gcp_function_location" validate:"required"`
GCPFunctionTimeout int `mapstructure:"gcp_function_timeout"`
}

type Actionner struct{}

func Register() *Actionner {
return new(Actionner)
}

func (a Actionner) Init() error {
return client.Init()
}

func (a Actionner) Information() models.Information {
return models.Information{
Name: Name,
FullName: Category + ":" + Name,
Category: Category,
Description: Description,
Source: Source,
RequiredOutputFields: RequiredOutputFields,
Permissions: Permissions,
Example: Example,
Continue: Continue,
AllowOutput: AllowOutput,
RequireOutput: RequireOutput,
}
}

func (a Actionner) Parameters() models.Parameters {
return Parameters{
GCPFunctionName: "",
GCPFunctionLocation: "us-central1", // Default location
}
}

func (a Actionner) Checks(_ *events.Event, action *rules.Action) error {
var parameters Parameters
err := utils.DecodeParams(action.GetParameters(), &parameters)
if err != nil {
return err
}

return checks.CheckFunctionExist{}.Run(parameters.GCPFunctionName, parameters.GCPFunctionLocation)
}

func (a Actionner) Run(event *events.Event, action *rules.Action) (utils.LogLine, *models.Data, error) {
gcpClient, err := client.GetGCPClient()
if err != nil {
return utils.LogLine{
Objects: nil,
Error: err.Error(),
Status: utils.FailureStr,
}, nil, err
}
return a.RunWithClient(gcpClient, event, action)
}

func (a Actionner) CheckParameters(action *rules.Action) error {
var parameters Parameters
err := utils.DecodeParams(action.GetParameters(), &parameters)
if err != nil {
return err
}

err = utils.ValidateStruct(parameters)
if err != nil {
return err
}
return nil
}

func (a Actionner) RunWithClient(c client.GCPClientAPI, event *events.Event, action *rules.Action) (utils.LogLine, *models.Data, error) {
var parameters Parameters
err := utils.DecodeParams(action.GetParameters(), &parameters)
if err != nil {
return utils.LogLine{
Objects: nil,
Error: err.Error(),
Status: utils.FailureStr,
}, nil, err
}

objects := map[string]string{
"name": parameters.GCPFunctionName,
"location": parameters.GCPFunctionLocation,
}

functionName := fmt.Sprintf("projects/%s/locations/%s/functions/%s", c.ProjectID(), parameters.GCPFunctionLocation, parameters.GCPFunctionName)

getFunctionReq := &functionspb.GetFunctionRequest{
Name: functionName,
}

gcpFunctionClient, err := c.GetGcpFunctionClient(context.Background())
if err != nil {
return utils.LogLine{
Objects: objects,
Error: err.Error(),
Status: utils.FailureStr,
}, nil, err
}

ctx := context.Background()

function, err := gcpFunctionClient.GetFunction(ctx, getFunctionReq)
if err != nil {
return utils.LogLine{
Objects: objects,
Error: fmt.Sprintf("failed to get function: %v", err),
Status: utils.FailureStr,
}, nil, err
}

if function.ServiceConfig.Uri == "" {
return utils.LogLine{
Objects: objects,
Error: "function does not have a valid URL",
Status: utils.FailureStr,
}, nil, fmt.Errorf("function does not have a valid URL")
}

functionURL := function.ServiceConfig.Uri

payload, err := json.Marshal(event)
if err != nil {
return utils.LogLine{
Objects: objects,
Error: err.Error(),
Status: utils.FailureStr,
}, nil, err
}

tokenSource, err := idtoken.NewTokenSource(ctx, functionURL)
if err != nil {
return utils.LogLine{
Objects: objects,
Error: fmt.Sprintf("failed to create ID token source: %v", err),
Status: utils.FailureStr,
}, nil, err
}
token, err := tokenSource.Token()
if err != nil {
return utils.LogLine{
Objects: objects,
Error: fmt.Sprintf("failed to obtain ID token: %v", err),
Status: utils.FailureStr,
}, nil, err
}

req, err := http.NewRequestWithContext(ctx, "POST", functionURL, bytes.NewReader(payload))
if err != nil {
return utils.LogLine{
Objects: objects,
Error: fmt.Sprintf("failed to create HTTP request: %v", err),
Status: utils.FailureStr,
}, nil, err
}

req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token.AccessToken)

if parameters.GCPFunctionTimeout > 0 {
httpClient := http.Client{
Timeout: time.Duration(parameters.GCPFunctionTimeout),
}
c.SetHTTPClient(&httpClient)
}

resp, err := c.HTTPClient().Do(req)
if err != nil {
return utils.LogLine{
Objects: objects,
Error: fmt.Sprintf("failed to invoke function: %v", err),
Status: utils.FailureStr,
}, nil, err
}
defer resp.Body.Close()

respBody, err := io.ReadAll(resp.Body)
if err != nil {
return utils.LogLine{
Objects: objects,
Error: fmt.Sprintf("failed to read response body: %v", err),
Status: utils.FailureStr,
}, nil, err
}

if resp.StatusCode != http.StatusOK {
return utils.LogLine{
Objects: objects,
Error: fmt.Sprintf("function invocation failed with status %d: %s", resp.StatusCode, string(respBody)),
Status: utils.FailureStr,
}, nil, fmt.Errorf("function invocation failed with status %d: %s", resp.StatusCode, string(respBody))
}

objects["function_response"] = string(respBody)
objects["function_response_status"] = strconv.Itoa(resp.StatusCode)

return utils.LogLine{
Objects: objects,
Status: utils.SuccessStr,
}, nil, nil
}
8 changes: 1 addition & 7 deletions actionners/kubernetes/drain/drain.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,13 +220,7 @@ func (a Actionner) RunWithClient(client k8s.DrainClient, event *events.Event, ac
go func(pod corev1.Pod) {
defer wg.Done()

ownerKind, err := k8s.GetOwnerKind(p)
if err != nil {
utils.PrintLog("warning", utils.LogLine{Message: fmt.Sprintf("error getting pod '%v' owner kind: %v", p.Name, err)})
atomic.AddInt32(&otherErrorsCount, 1)
return
}

ownerKind := k8s.PodKind(p)
switch ownerKind {
case utils.DaemonSetStr:
if parameters.IgnoreDaemonsets {
Expand Down
38 changes: 20 additions & 18 deletions actionners/kubernetes/terminate/terminate.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ rules:
grace_period_seconds: 5
ignore_daemonsets: true
ignore_statefulsets: true
ignore_standalone_pods: true
min_healthy_replicas: 33%
`
)
Expand All @@ -59,10 +60,11 @@ var (
)

type Parameters struct {
MinHealthyReplicas string `mapstructure:"min_healthy_replicas" validate:"omitempty,is_absolut_or_percent"`
IgnoreDaemonsets bool `mapstructure:"ignore_daemonsets" validate:"omitempty"`
IgnoreStatefulSets bool `mapstructure:"ignore_statefulsets" validate:"omitempty"`
GracePeriodSeconds int `mapstructure:"grace_period_seconds" validate:"omitempty"`
MinHealthyReplicas string `mapstructure:"min_healthy_replicas" validate:"omitempty,is_absolut_or_percent"`
IgnoreDaemonsets bool `mapstructure:"ignore_daemonsets" validate:"omitempty"`
IgnoreStatefulSets bool `mapstructure:"ignore_statefulsets" validate:"omitempty"`
IgnoreStandalonePods bool `mapstructure:"ignore_standalone_pods" validate:"omitempty"`
GracePeriodSeconds int `mapstructure:"grace_period_seconds" validate:"omitempty"`
}

type Actionner struct{}
Expand Down Expand Up @@ -92,10 +94,11 @@ func (a Actionner) Information() models.Information {
}
func (a Actionner) Parameters() models.Parameters {
return Parameters{
MinHealthyReplicas: "",
IgnoreDaemonsets: false,
IgnoreStatefulSets: false,
GracePeriodSeconds: 0,
MinHealthyReplicas: "",
IgnoreDaemonsets: false,
IgnoreStatefulSets: false,
IgnoreStandalonePods: true,
GracePeriodSeconds: 0,
}
}

Expand Down Expand Up @@ -137,16 +140,7 @@ func (a Actionner) Run(event *events.Event, action *rules.Action) (utils.LogLine
err
}

ownerKind, err := k8s.GetOwnerKind(*pod)
if err != nil {
return utils.LogLine{
Objects: objects,
Error: err.Error(),
Status: utils.FailureStr,
},
nil,
err
}
ownerKind := k8s.PodKind(*pod)

switch ownerKind {
case utils.DaemonSetStr:
Expand Down Expand Up @@ -226,6 +220,14 @@ func (a Actionner) Run(event *events.Event, action *rules.Action) (utils.LogLine
}
}
}
case utils.StandalonePodStr:
if parameters.IgnoreStandalonePods {
return utils.LogLine{
Objects: objects,
Status: "ignored",
Result: fmt.Sprintf("the pod '%v' in the namespace '%v' is a standalone pod and will be ignored.", podName, namespace),
}, nil, nil
}
}

err = client.Clientset.CoreV1().Pods(namespace).Delete(context.Background(), podName, metav1.DeleteOptions{GracePeriodSeconds: gracePeriodSeconds})
Expand Down
Loading
Loading