diff --git a/plugins/k8saudit-ovh/.gitignore b/plugins/k8saudit-ovh/.gitignore new file mode 100644 index 00000000..f88a9453 --- /dev/null +++ b/plugins/k8saudit-ovh/.gitignore @@ -0,0 +1,3 @@ +libk8saudit-ovh.so +.vscode +falco.yaml \ No newline at end of file diff --git a/plugins/k8saudit-ovh/CHANGELOG.md b/plugins/k8saudit-ovh/CHANGELOG.md new file mode 100644 index 00000000..b516979a --- /dev/null +++ b/plugins/k8saudit-ovh/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## v0.1.0 + +* First version of the `k8saudit-ovh` plugin 🎉 diff --git a/plugins/k8saudit-ovh/Makefile b/plugins/k8saudit-ovh/Makefile new file mode 100644 index 00000000..82c2501f --- /dev/null +++ b/plugins/k8saudit-ovh/Makefile @@ -0,0 +1,22 @@ +SHELL=/bin/bash -o pipefail +GO ?= go + +NAME := k8saudit-ovh +OUTPUT := lib$(NAME).so + +ifeq ($(DEBUG), 1) + GODEBUGFLAGS= GODEBUG=cgocheck=1 +else + GODEBUGFLAGS= GODEBUG=cgocheck=0 +endif + +all: build + +clean: + @rm -f lib$(NAME).so + +build: clean + @$(GODEBUGFLAGS) $(GO) build -buildmode=c-shared -buildvcs=false -o $(OUTPUT) ./plugin + +install: + sudo cp $(OUTPUT) /usr/share/falco/plugins/ \ No newline at end of file diff --git a/plugins/k8saudit-ovh/OWNERS b/plugins/k8saudit-ovh/OWNERS new file mode 100644 index 00000000..e0e1017c --- /dev/null +++ b/plugins/k8saudit-ovh/OWNERS @@ -0,0 +1,3 @@ +approvers: + - scraly + - Issif diff --git a/plugins/k8saudit-ovh/README.md b/plugins/k8saudit-ovh/README.md new file mode 100644 index 00000000..6d123d54 --- /dev/null +++ b/plugins/k8saudit-ovh/README.md @@ -0,0 +1,218 @@ +# Kubernetes Audit Events Plugin for OVHcloud + +## Introduction + +This plugin extends Falco to support [Kubernetes Audit Events](https://kubernetes.io/docs/tasks/debug-application-cluster/audit/#audit-backends) from OVHcloud MKS clusters as a new data source. +For more details about what Audit logs are, see the [README of k8saudit plugin](https://github.com/falcosecurity/plugins/blob/main/plugins/k8saudit/README.md). + +### Functionality + +This plugin supports consuming Kubernetes Audit Events stored in OVHcloud Log Data Platform (LDP) for the MKS Clusters, see [OVHcloud official documentation](https://help.ovhcloud.com/csm/fr-public-cloud-kubernetes-forwarding-audit-logs?id=kb_article_view&sysparm_article=KB0062284) for details. + +## Capabilities + +The `k8saudit-ovh` uses the field extraction methods of the [`k8saudit`](https://github.com/falcosecurity/plugins/tree/main/plugins/k8saudit) plugin as the format for the Audit Logs is same. + +### Event Source + +The event source for Kubernetes Audit Events from OVHcloud is `k8s_audit`, it allows to use same rules than `k8saudit` plugin. + +### Supported Fields + +Here is the current set of supported fields (from `k8saudit` plugin's extractor): + +| NAME | TYPE | ARG | DESCRIPTION | +|----------------------------------------------------|-----------------|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `ka.auditid` | `string` | None | The unique id of the audit event | +| `ka.stage` | `string` | None | Stage of the request (e.g. RequestReceived, ResponseComplete, etc.) | +| `ka.auth.decision` | `string` | None | The authorization decision | +| `ka.auth.reason` | `string` | None | The authorization reason | +| `ka.auth.openshift.decision` | `string` | None | The authentication decision of the openshfit apiserver extention. Only available on openshift clusters | +| `ka.auth.openshift.username` | `string` | None | The user name performing the openshift authentication operation. Only available on openshift clusters | +| `ka.user.name` | `string` | None | The user name performing the request | +| `ka.user.groups` | `string (list)` | None | The groups to which the user belongs | +| `ka.impuser.name` | `string` | None | The impersonated user name | +| `ka.verb` | `string` | None | The action being performed | +| `ka.uri` | `string` | None | The request URI as sent from client to server | +| `ka.uri.param` | `string` | Key, Required | The value of a given query parameter in the uri (e.g. when uri=/foo?key=val, ka.uri.param[key] is val). | +| `ka.target.name` | `string` | None | The target object name | +| `ka.target.namespace` | `string` | None | The target object namespace | +| `ka.target.resource` | `string` | None | The target object resource | +| `ka.target.subresource` | `string` | None | The target object subresource | +| `ka.target.pod.name` | `string` | None | The target pod name | +| `ka.req.binding.subjects` | `string (list)` | None | When the request object refers to a cluster role binding, the subject (e.g. account/users) being linked by the binding | +| `ka.req.binding.role` | `string` | None | When the request object refers to a cluster role binding, the role being linked by the binding | +| `ka.req.binding.subject.has_name` | `string` | Key, Required | Deprecated, always returns "N/A". Only provided for backwards compatibility | +| `ka.req.configmap.name` | `string` | None | If the request object refers to a configmap, the configmap name | +| `ka.req.configmap.obj` | `string` | None | If the request object refers to a configmap, the entire configmap object | +| `ka.req.pod.containers.image` | `string (list)` | Index | When the request object refers to a pod, the container's images. | +| `ka.req.container.image` | `string` | None | Deprecated by ka.req.pod.containers.image. Returns the image of the first container only | +| `ka.req.pod.containers.image.repository` | `string (list)` | Index | The same as req.container.image, but only the repository part (e.g. falcosecurity/falco). | +| `ka.req.container.image.repository` | `string` | None | Deprecated by ka.req.pod.containers.image.repository. Returns the repository of the first container only | +| `ka.req.pod.host_ipc` | `string` | None | When the request object refers to a pod, the value of the hostIPC flag. | +| `ka.req.pod.host_network` | `string` | None | When the request object refers to a pod, the value of the hostNetwork flag. | +| `ka.req.container.host_network` | `string` | None | Deprecated alias for ka.req.pod.host_network | +| `ka.req.pod.host_pid` | `string` | None | When the request object refers to a pod, the value of the hostPID flag. | +| `ka.req.pod.containers.host_port` | `string (list)` | Index | When the request object refers to a pod, all container's hostPort values. | +| `ka.req.pod.containers.privileged` | `string (list)` | Index | When the request object refers to a pod, the value of the privileged flag for all containers. | +| `ka.req.container.privileged` | `string` | None | Deprecated by ka.req.pod.containers.privileged. Returns true if any container has privileged=true | +| `ka.req.pod.containers.allow_privilege_escalation` | `string (list)` | Index | When the request object refers to a pod, the value of the allowPrivilegeEscalation flag for all containers | +| `ka.req.pod.containers.read_only_fs` | `string (list)` | Index | When the request object refers to a pod, the value of the readOnlyRootFilesystem flag for all containers | +| `ka.req.pod.run_as_user` | `string` | None | When the request object refers to a pod, the runAsUser uid specified in the security context for the pod. See ....containers.run_as_user for the runAsUser for individual containers | +| `ka.req.pod.containers.run_as_user` | `string (list)` | Index | When the request object refers to a pod, the runAsUser uid for all containers | +| `ka.req.pod.containers.eff_run_as_user` | `string (list)` | Index | When the request object refers to a pod, the initial uid that will be used for all containers. This combines information from both the pod and container security contexts and uses 0 if no uid is specified | +| `ka.req.pod.run_as_group` | `string` | None | When the request object refers to a pod, the runAsGroup gid specified in the security context for the pod. See ....containers.run_as_group for the runAsGroup for individual containers | +| `ka.req.pod.containers.run_as_group` | `string (list)` | Index | When the request object refers to a pod, the runAsGroup gid for all containers | +| `ka.req.pod.containers.eff_run_as_group` | `string (list)` | Index | When the request object refers to a pod, the initial gid that will be used for all containers. This combines information from both the pod and container security contexts and uses 0 if no gid is specified | +| `ka.req.pod.containers.proc_mount` | `string (list)` | Index | When the request object refers to a pod, the procMount types for all containers | +| `ka.req.role.rules` | `string (list)` | None | When the request object refers to a role/cluster role, the rules associated with the role | +| `ka.req.role.rules.apiGroups` | `string (list)` | Index | When the request object refers to a role/cluster role, the api groups associated with the role's rules | +| `ka.req.role.rules.nonResourceURLs` | `string (list)` | Index | When the request object refers to a role/cluster role, the non resource urls associated with the role's rules | +| `ka.req.role.rules.verbs` | `string (list)` | Index | When the request object refers to a role/cluster role, the verbs associated with the role's rules | +| `ka.req.role.rules.resources` | `string (list)` | Index | When the request object refers to a role/cluster role, the resources associated with the role's rules | +| `ka.req.pod.fs_group` | `string` | None | When the request object refers to a pod, the fsGroup gid specified by the security context. | +| `ka.req.pod.supplemental_groups` | `string (list)` | None | When the request object refers to a pod, the supplementalGroup gids specified by the security context. | +| `ka.req.pod.containers.add_capabilities` | `string (list)` | Index | When the request object refers to a pod, all capabilities to add when running the container. | +| `ka.req.service.type` | `string` | None | When the request object refers to a service, the service type | +| `ka.req.service.ports` | `string (list)` | Index | When the request object refers to a service, the service's ports | +| `ka.req.pod.volumes.hostpath` | `string (list)` | Index | When the request object refers to a pod, all hostPath paths specified for all volumes | +| `ka.req.volume.hostpath` | `string` | Key, Required | Deprecated by ka.req.pod.volumes.hostpath. Return true if the provided (host) path prefix is used by any volume | +| `ka.req.pod.volumes.flexvolume_driver` | `string (list)` | Index | When the request object refers to a pod, all flexvolume drivers specified for all volumes | +| `ka.req.pod.volumes.volume_type` | `string (list)` | Index | When the request object refers to a pod, all volume types for all volumes | +| `ka.resp.name` | `string` | None | The response object name | +| `ka.response.code` | `string` | None | The response code | +| `ka.response.reason` | `string` | None | The response reason (usually present only for failures) | +| `ka.useragent` | `string` | None | The useragent of the client who made the request to the apiserver | +| `ka.sourceips` | `string (list)` | Index | The IP addresses of the client who made the request to the apiserver | +| `ka.cluster.name` | `string` | None | The name of the k8s cluster | + + +## Usage + +### Install the plugin + +```bash +# Add falcosecurity index +sudo falcoctl index add falcosecurity https://falcosecurity.github.io/falcoctl/index.yaml +# Install k8saudit-ovh Falco plugin +sudo falcoctl artifact install k8saudit-ovh +``` + +### Configuration + +Here's an example of configuration of `falco.yaml`: + +```yaml +plugins: + - name: k8saudit-ovh + library_path: /usr/share/falco/plugins/libk8saudit-ovh.so + open_params: "" # gra.logs.ovh.com/tail/?tk= + - name: json + library_path: libjson.so + init_config: "" +load_plugins: [k8saudit-ovh, json] +``` +**Open Parameters** + +A string which contains the LDP WebSocket URL of your OVHcloud MKS Cluster (required). + +[Follow this guide](https://help.ovhcloud.com/csm/fr-logs-data-platform-ldp-tail?id=kb_article_view&sysparm_article=KB0037675#retrieve-your-websocket-address) to retrieve the OVHcloud LDP URL. + +### Rules + +The `k8saudit-ovh` plugin ships with no default rule for test purpose, you can use the same rules than those for `k8saudit` plugin. See [here](https://github.com/falcosecurity/plugins/blob/main/plugins/k8saudit/rules/k8s_audit_rules.yaml). + + +To test if it works anyway, you can still use this one for example: + +```yaml +- required_engine_version: 15 +- required_plugin_versions: + - name: k8saudit-ovh + version: 0.1.0 + +- rule: TEST + desc: > + Test rule + condition: > + ka.verb in (get,create,delete,update) + output: verb=%ka.verb name=%ka.target.name resp=%ka.response.code namespace=%ka.target.namespace + priority: NOTICE + source: k8s_audit + tags: [k8s] +``` + +### Running locally + +This plugin requires Falco with version >= **0.35.0**. + +```bash +falco -c falco.yaml -r rules/k8s_audit_rules.yaml +``` + +```bash +12:29:51.895849000: Notice verb=get name= resp=200 namespace= +12:29:51.900789000: Notice verb=get name= resp=200 namespace= +^CEvents detected: 2 +Rule counts by severity: + NOTICE: 2 +Triggered rules by rule name: + TEST: 2 +``` + +### Running in an OVHcloud MKS cluster + +You can use the official [Falco Helm chart](https://github.com/falcosecurity/charts/tree/master/falco) to deploy it, with the following `values.yaml`: + +```yaml +tty: true +kubernetes: false + +# Just a Deployment with 1 replica (instead of a Daemonset) to have only one Pod that pulls the MKS Audit Logs from a OVHcloud LDP +controller: + kind: deployment + deployment: + replicas: 1 + +falco: + rules_files: + - /etc/falco/k8s_audit_rules.yaml + - /etc/falco/rules.d + plugins: + - name: k8saudit-ovh + library_path: libk8saudit-ovh.so + open_params: "gra.logs.ovh.com/tail/?tk=" # Replace with your LDP Websocket URL + - name: json + library_path: libjson.so + init_config: "" + # Plugins that Falco will load. Note: the same plugins are installed by the falcoctl-artifact-install init container. + load_plugins: [k8saudit-ovh, json] + +driver: + enabled: false +collectors: + enabled: false + +# use falcoctl to install automatically the plugin and the rules +falcoctl: + artifact: + install: + enabled: true + follow: + enabled: true + config: + indexes: + - name: falcosecurity + url: https://falcosecurity.github.io/falcoctl/index.yaml + artifact: + allowedTypes: + - plugin + - rulesfile + install: + resolveDeps: false + refs: [k8saudit-rules:0, k8saudit-ovh:0.1, json:0] + follow: + refs: [k8saudit-rules:0] +``` + +Note: You can also install Falcosidekick and enable the webUI to watch your events in an interface. diff --git a/plugins/k8saudit-ovh/go.mod b/plugins/k8saudit-ovh/go.mod new file mode 100644 index 00000000..4b52e95f --- /dev/null +++ b/plugins/k8saudit-ovh/go.mod @@ -0,0 +1,16 @@ +module github.com/falcosecurity/plugins/plugins/k8saudit-ovh + +go 1.23.3 + +require ( + github.com/falcosecurity/plugin-sdk-go v0.7.4 + github.com/falcosecurity/plugins/plugins/k8saudit v0.11.0 + github.com/gorilla/websocket v1.5.3 + golang.org/x/net v0.32.0 +) + +require ( + github.com/alecthomas/jsonschema v0.0.0-20220216202328-9eeeec9d044b // indirect + github.com/iancoleman/orderedmap v0.3.0 // indirect + github.com/valyala/fastjson v1.6.4 // indirect +) diff --git a/plugins/k8saudit-ovh/go.sum b/plugins/k8saudit-ovh/go.sum new file mode 100644 index 00000000..89bef964 --- /dev/null +++ b/plugins/k8saudit-ovh/go.sum @@ -0,0 +1,37 @@ +github.com/alecthomas/jsonschema v0.0.0-20220216202328-9eeeec9d044b h1:doCpXjVwui6HUN+xgNsNS3SZ0/jUZ68Eb+mJRNOZfog= +github.com/alecthomas/jsonschema v0.0.0-20220216202328-9eeeec9d044b/go.mod h1:/n6+1/DWPltRLWL/VKyUxg6tzsl5kHUCcraimt4vr60= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/falcosecurity/plugin-sdk-go v0.7.4 h1:iNV0pgWgJwOHqSCjTw4Hsvtu5WuwoqckAWzpIEy9giQ= +github.com/falcosecurity/plugin-sdk-go v0.7.4/go.mod h1:NP+y22DYOS+G3GDXIXNmzf0CBL3nfPPMoQuHvAzfitQ= +github.com/falcosecurity/plugins/plugins/k8saudit v0.11.0 h1:ywwQ8kQmMS0HL3PuwBSKUmERqePrCSnajxnSCNC0HQY= +github.com/falcosecurity/plugins/plugins/k8saudit v0.11.0/go.mod h1:RmSc1za6asI52w3uVhZGb/p6RoQr2OWmp/Zc8+kiMWw= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA= +github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= +github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= +github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= +golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/plugins/k8saudit-ovh/pkg/k8sauditovh/k8sauditovh.go b/plugins/k8saudit-ovh/pkg/k8sauditovh/k8sauditovh.go new file mode 100644 index 00000000..9cb4d53b --- /dev/null +++ b/plugins/k8saudit-ovh/pkg/k8sauditovh/k8sauditovh.go @@ -0,0 +1,190 @@ +package k8sauditovh + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "net" + "net/http" + "net/url" + "os" + "text/template" + "time" + + "github.com/falcosecurity/plugin-sdk-go/pkg/sdk" + "github.com/falcosecurity/plugin-sdk-go/pkg/sdk/plugins" + "github.com/falcosecurity/plugin-sdk-go/pkg/sdk/plugins/source" + "github.com/falcosecurity/plugins/plugins/k8saudit/pkg/k8saudit" + + "github.com/gorilla/websocket" +) + +var ( + ID uint32 + Name string + Description string + Contact string + Version string + EventSource string +) + +const ( + pluginName = "k8saudit-ovh" + + // Time allowed to read the next pong message from the client. + pongWait = 60 * time.Second +) + +type PluginConfig struct { + MaxEventSize uint64 `json:"maxEventSize" jsonschema:"title=Maximum event size,description=Maximum size of single audit event (Default: 262144),default=262144"` +} + +// Plugin represents our plugin +type Plugin struct { + k8saudit.Plugin + Logger *log.Logger + Config PluginConfig +} + +// Resets sets the configuration to its default values +func (k *PluginConfig) Reset() { + k.MaxEventSize = uint64(sdk.DefaultEvtSize) +} + +// SetInfo is used to set the Info of the plugin +func (p *Plugin) SetInfo(id uint32, name, description, contact, version, eventSource string) { + ID = id + Name = name + Contact = contact + Version = version + EventSource = eventSource +} + +// Info displays information of the plugin to Falco plugin framework +func (p *Plugin) Info() *plugins.Info { + return &plugins.Info{ + ID: ID, + Name: Name, + Description: Description, + Contact: Contact, + Version: Version, + EventSource: EventSource, + } +} + +// Init is called by the Falco plugin framework as first entry, +// we use it for setting default configuration values and mapping +// values from `init_config` (json format for this plugin) +func (p *Plugin) Init(config string) error { + p.Plugin.Config.Reset() + p.Config.Reset() + p.Logger = log.New(os.Stderr, "["+pluginName+"] ", log.LstdFlags|log.LUTC|log.Lmsgprefix) + return nil +} + +func (p *Plugin) OpenParams() ([]sdk.OpenParam, error) { + return []sdk.OpenParam{ + {Value: "", Desc: "The LDP Websocket URL to use to get the OVHcloud MKS Audit Logs sent to a LDP data stream"}, + }, nil +} + +// Open is called by Falco plugin framework for opening a stream of events, we call that an instance +func (p *Plugin) Open(ovhLDPURL string) (source.Instance, error) { + t, err := template.New("template").Funcs(template.FuncMap{ + "color": color, + "bColor": bColor, + "noColor": func() string { return color("reset") }, + "date": date, + "join": join, + "concat": concat, + "duration": duration, + "int": toInt, + "float": toFloat, + "string": toString, + "get": get, + "column": column, + "begin": begin, + "contain": contain, + "level": level, + }).Parse("{{._appID}}> {{.short_message}}") + if err != nil { + p.Logger.Fatalf("Failed to parse pattern: %s", err.Error()) + } + + if ovhLDPURL == "" { + return nil, fmt.Errorf("OVHcloud LDP URL can't be empty") + } + + eventC := make(chan source.PushEvent) + + go func() { + defer close(eventC) + + u := url.URL{Scheme: "wss", Host: ovhLDPURL, Path: ""} + v, _ := url.QueryUnescape(u.String()) + + headers := make(http.Header) + // headers.Set("Origin", "http://mySelf") + wsChan, _, err := websocket.DefaultDialer.Dial(v, headers) + if err != nil { + eventC <- source.PushEvent{Err: err} + return + } + defer wsChan.Close() + + for { + //wsChan.SetReadDeadline(time.Now().Add(5 * time.Second)) + wsChan.SetReadDeadline(time.Now().Add(pongWait)) + _, msg, err := wsChan.ReadMessage() + + // Keep the WebSocket connection alive + if t, ok := err.(net.Error); ok && t.Timeout() { + // Timeout, send a Ping && continue + if err := wsChan.WriteMessage(websocket.PingMessage, nil); err != nil { + p.Logger.Println("The end host probably closed the connection", err.Error()) + } + continue + } + + if err != nil { + p.Logger.Printf("Error while reading from %q: %q. Will try to reconnect after 1s...\n", u.Host, err.Error()) + time.Sleep(1 * time.Second) + break + } + + // Extract Message + var logMessage struct { + Message string `json:"message"` + } + json.Unmarshal(msg, &logMessage) + + // Extract infos + var message map[string]interface{} + json.Unmarshal([]byte(logMessage.Message), &message) + + var m bytes.Buffer + err = t.Execute(&m, message) + if err != nil { + p.Logger.Println(err) + continue + } + + // Parse audit events payload thanks to k8saudit extract parse and extract methods + values, err := p.Plugin.ParseAuditEventsPayload([]byte(m.String())[12:]) + if err != nil { + p.Logger.Println(err) + continue + } + for _, j := range values { + if j.Err != nil { + p.Logger.Println(j.Err) + continue + } + + eventC <- *j + } + } + }() + return source.NewPushInstance(eventC) +} diff --git a/plugins/k8saudit-ovh/pkg/k8sauditovh/template.go b/plugins/k8saudit-ovh/pkg/k8sauditovh/template.go new file mode 100644 index 00000000..638c7191 --- /dev/null +++ b/plugins/k8saudit-ovh/pkg/k8sauditovh/template.go @@ -0,0 +1,226 @@ +package k8sauditovh + +import ( + "bytes" + "fmt" + "strconv" + "strings" + "time" +) + +var bColors = map[string][]byte{ + "green": {27, 91, 52, 50, 109}, + "white": {27, 91, 52, 55, 109}, + "yellow": {27, 91, 52, 51, 109}, + "red": {27, 91, 52, 49, 109}, + "blue": {27, 91, 52, 52, 109}, + "magenta": {27, 91, 52, 53, 109}, + "cyan": {27, 91, 52, 54, 109}, + "reset": {27, 91, 48, 109}, +} + +func bColor(c string) string { + if s, ok := bColors[c]; ok { + return string(s) + } + return "" +} + +var colors = map[string][]byte{ + "green": {27, 91, 51, 50, 109}, + "white": {27, 91, 51, 55, 109}, + "yellow": {27, 91, 51, 51, 109}, + "red": {27, 91, 51, 49, 109}, + "blue": {27, 91, 51, 52, 109}, + "magenta": {27, 91, 51, 53, 109}, + "cyan": {27, 91, 51, 54, 109}, + "reset": {27, 91, 48, 109}, +} + +func color(c string) string { + if s, ok := colors[c]; ok { + return string(s) + } + return "" +} + +func date(v float64, f ...string) string { + + t := time.Unix(int64(v), 0) + + if len(f) == 0 { + return t.Format("2006-01-02 15:04:05") + } + + return t.Format(f[0]) +} + +func join(s ...string) string { + return strings.Join(s[1:], s[0]) +} + +func concat(s ...string) string { + var b bytes.Buffer + for _, v := range s { + b.WriteString(v) + } + return b.String() +} + +func duration(v interface{}, factor float64) (string, error) { + var d time.Duration + switch value := v.(type) { + case string: + f, err := strconv.ParseFloat(value, 64) + if err != nil { + return "", err + } + d = time.Duration(f * factor) + case float64: + d = time.Duration(value * factor) + case int64: + d = time.Duration(value * int64(factor)) + default: + return "", fmt.Errorf("Invalid type %T for duration", v) + } + return d.String(), nil +} + +func get(v map[string]interface{}, k string) interface{} { + return v[k] +} + +var columnLength []int + +func column(sep string, s ...string) (string, error) { + if columnLength == nil { + columnLength = make([]int, len(s)) + } + + if len(s) != len(columnLength) { + return "", fmt.Errorf("Invalid number of arguments to 'column'") + } + + for k, v := range s { + if len(v) > columnLength[k] { + columnLength[k] = len(v) + } else { + s[k] = v + strings.Repeat(" ", columnLength[k]-len(v)) + } + } + + return strings.Join(s, sep), nil +} + +func begin(v interface{}, substr string) bool { + var value string + + switch v.(type) { + case string: + value = v.(string) + default: + value = fmt.Sprintf("%v", v) + } + return strings.HasPrefix(value, substr) +} + +func contain(v interface{}, substr string) bool { + var value string + + switch v.(type) { + case string: + value = v.(string) + default: + value = fmt.Sprintf("%v", v) + } + return strings.Contains(value, substr) +} + +var syslogLevels = map[int]string{ + 0: "emerg", + 1: "alert", + 2: "crit", + 3: "err", + 4: "warn", + 5: "notice", + 6: "info", + 7: "debug", +} + +func level(v interface{}) (string, error) { + vFloat, err := toNumber(v) + if err != nil { + return "", err + } + + value, ok := syslogLevels[int(vFloat)] + if !ok { + value = fmt.Sprintf("(invalid:%d)", int(vFloat)) + } + + return value, nil +} + +func toInt(v interface{}) (int64, error) { + if f, ok := v.(float64); ok { + return int64(f), nil + } + if s, ok := v.(string); ok { + f, e := strconv.ParseFloat(s, 64) + return int64(f), e + } + return 0, fmt.Errorf("Invalid type %T for conversion to `int`", v) +} + +func toFloat(v interface{}) (float64, error) { + if f, ok := v.(float64); ok { + return f, nil + } + if s, ok := v.(string); ok { + f, e := strconv.ParseFloat(s, 64) + return f, e + } + return 0, fmt.Errorf("Invalid type %T for conversion to `float`", v) +} + +func toString(v interface{}) string { + return fmt.Sprintf("%v", v) +} + +func toNumber(v interface{}) (float64, error) { + switch value := v.(type) { + case string: + // Try to parse value as float64 + f, err := strconv.ParseFloat(value, 64) + if err != nil { + return 0, fmt.Errorf("'%v' can't be parsed as a number", v) + } + return f, nil + case uint: + return float64(value), nil + case uint8: + return float64(value), nil + case uint16: + return float64(value), nil + case uint32: + return float64(value), nil + case uint64: + return float64(value), nil + case int: + return float64(value), nil + case int8: + return float64(value), nil + case int16: + return float64(value), nil + case int32: + return float64(value), nil + case int64: + return float64(value), nil + case float32: + return float64(value), nil + case float64: + return value, nil + default: + return 0, fmt.Errorf("can't parse type %T as a number", v) + } +} diff --git a/plugins/k8saudit-ovh/plugin/main.go b/plugins/k8saudit-ovh/plugin/main.go new file mode 100644 index 00000000..048c80b9 --- /dev/null +++ b/plugins/k8saudit-ovh/plugin/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "github.com/falcosecurity/plugin-sdk-go/pkg/sdk/plugins" + "github.com/falcosecurity/plugin-sdk-go/pkg/sdk/plugins/extractor" + "github.com/falcosecurity/plugin-sdk-go/pkg/sdk/plugins/source" + "github.com/falcosecurity/plugins/plugins/k8saudit-ovh/pkg/k8sauditovh" +) + +const ( + PluginID uint32 = 22 + PluginName = "k8saudit-ovh" + PluginDescription = "Read Kubernetes Audit Events for OVHcloud MKS" + PluginContact = "github.com/falcosecurity/plugins" + PluginVersion = "0.1.0" + PluginEventSource = "k8s_audit" +) + +func init() { + plugins.SetFactory(func() plugins.Plugin { + p := &k8sauditovh.Plugin{} + p.SetInfo( + PluginID, + PluginName, + PluginDescription, + PluginContact, + PluginVersion, + PluginEventSource, + ) + extractor.Register(p) + source.Register(p) + return p + }) +} + +func main() {} diff --git a/registry.yaml b/registry.yaml index d84cc530..86efe1ca 100644 --- a/registry.yaml +++ b/registry.yaml @@ -519,14 +519,18 @@ plugins: - name: k8saudit-ovh description: Read Kubernetes Audit Events from OVHcloud MKS Clusters - authors: Aurélie Vache - contact: https://scraly.com/ + authors: The Falco Authors + contact: https://falco.org/community maintainers: - - name: Aurélie Vache - email: scraly@gmail.com - url: https://github.com/scraly/k8saudit-ovh/ + - name: The Falco Authors + email: cncf-falco-dev@lists.cncf.io + url: https://github.com/falcosecurity/plugins/tree/main/plugins/k8saudit-ovh rules_url: https://github.com/falcosecurity/plugins/tree/main/plugins/k8saudit/rules license: Apache-2.0 + signature: + cosign: + certificate-oidc-issuer: https://token.actions.githubusercontent.com + certificate-identity-regexp: https://github.com/falcosecurity/plugins/ keywords: - audit - audit-log