├── .air.toml ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── lint.yml │ ├── main.yml │ ├── pr.yaml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── .ko.yaml ├── CHANGELOG.md ├── LICENSE ├── OWNERS ├── README.md ├── actionners ├── actionners.go ├── aws │ └── lambda │ │ └── lambda.go ├── calico │ └── networkpolicy │ │ └── networkpolicy.go ├── cilium │ └── networkpolicy │ │ └── networkpolicy.go ├── gcp │ └── function │ │ └── function.go └── kubernetes │ ├── annotation │ └── annotation.go │ ├── cordon │ └── cordon.go │ ├── delete │ └── delete.go │ ├── download │ └── download.go │ ├── drain │ └── drain.go │ ├── exec │ └── exec.go │ ├── helpers │ └── helpers.go │ ├── label │ └── label.go │ ├── log │ └── log.go │ ├── networkpolicy │ └── networkpolicy.go │ ├── script │ └── script.go │ ├── sysdig │ └── sysdig.go │ ├── tcpdump │ └── tcpdump.go │ └── terminate │ └── terminate.go ├── cmd ├── actionners.go ├── notifiers.go ├── outputs.go ├── root.go ├── rules.go ├── server.go └── version.go ├── config_example.yaml ├── configuration ├── configuration.go └── version.go ├── deployment └── compose │ ├── docker-compose.yaml │ ├── grafana │ ├── grafana.ini │ └── provisioning │ │ ├── dashboards │ │ └── sample-node.json │ │ └── datasources │ │ └── default.yaml │ ├── loki │ └── loki.yaml │ ├── otel │ └── otel-collector.yaml │ ├── prometheus │ └── prometheus.yaml │ └── tempo │ └── tempo.yaml ├── examples.txt ├── go.mod ├── go.sum ├── internal ├── aws │ ├── checks │ │ └── checks.go │ └── client │ │ └── client.go ├── calico │ └── client │ │ └── client.go ├── cilium │ └── client │ │ └── client.go ├── context │ ├── aws │ │ └── aws.go │ ├── context.go │ └── kubernetes │ │ └── kubernetes.go ├── events │ └── events.go ├── gcp │ ├── checks │ │ └── checks.go │ └── client │ │ └── client.go ├── handler │ └── handler.go ├── kubernetes │ ├── checks │ │ └── checks.go │ └── client │ │ └── client.go ├── minio │ └── client │ │ └── client.go ├── models │ └── models.go ├── nats │ └── nats.go ├── otlp │ ├── metrics │ │ └── metrics.go │ └── traces │ │ └── traces.go └── rules │ ├── priority.go │ └── rules.go ├── mageFile.go ├── main.go ├── notifiers ├── elasticsearch │ ├── elasticsearch.go │ └── mapping.go ├── http │ └── client.go ├── k8sevents │ └── k8sevents.go ├── loki │ └── loki.go ├── notifiers.go ├── slack │ └── slack.go ├── smtp │ ├── smtp.go │ └── templates.go └── webhook │ └── webhook.go ├── outputs ├── aws │ └── s3 │ │ └── s3.go ├── file │ └── file.go ├── gcs │ └── gcs.go ├── minio │ └── minio.go └── outputs.go ├── rules.yaml ├── rules_override.yaml └── utils └── utils.go /.air.toml: -------------------------------------------------------------------------------- 1 | root = "." 2 | testdata_dir = "testdata" 3 | tmp_dir = "tmp" 4 | 5 | [build] 6 | args_bin = [] 7 | bin = "./tmp/main" 8 | cmd = "go build -o ./tmp/main ." 9 | delay = 1000 10 | exclude_dir = ["assets", "tmp", "vendor", "testdata"] 11 | exclude_file = [] 12 | exclude_regex = ["_test.go"] 13 | exclude_unchanged = false 14 | follow_symlink = false 15 | full_bin = "" 16 | include_dir = [] 17 | include_ext = ["go", "tpl", "tmpl", "html"] 18 | include_file = [] 19 | kill_delay = "0s" 20 | log = "build-errors.log" 21 | poll = false 22 | poll_interval = 0 23 | post_cmd = [] 24 | pre_cmd = [] 25 | rerun = true 26 | rerun_delay = 500 27 | send_interrupt = false 28 | stop_on_error = false 29 | 30 | [color] 31 | app = "" 32 | build = "yellow" 33 | main = "magenta" 34 | runner = "green" 35 | watcher = "cyan" 36 | 37 | [log] 38 | main_only = false 39 | time = false 40 | 41 | [misc] 42 | clean_on_exit = false 43 | 44 | [proxy] 45 | app_port = 0 46 | enabled = false 47 | proxy_port = 0 48 | 49 | [screen] 50 | clear_on_rebuild = false 51 | keep_scroll = true 52 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | **What type of PR is this?** 9 | 10 | > Uncomment one (or more) `/kind <>` lines: 11 | 12 | > /kind bug 13 | > /kind cleanup 14 | > /kind design 15 | > /kind documentation 16 | > /kind enhancement 17 | > /kind failing-test 18 | > /kind feature 19 | 20 | 23 | 24 | **Any specific area of the project related to this PR?** 25 | 26 | > Uncomment one (or more) `/area <>` lines: 27 | 28 | > /area actionners 29 | > /area build 30 | > /area config 31 | > /area context 32 | > /area core 33 | > /area notifiers 34 | > /area ouputs 35 | > /area rule-engine 36 | 37 | 40 | 41 | **What this PR does / Why we need it**: 42 | 43 | **How to reproduce the issue**: 44 | 45 | **Which issue(s) this PR fixes**: 46 | 47 | 52 | 53 | **Special notes for your reviewer**: 54 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: gomod 5 | directory: "/" 6 | schedule: 7 | interval: daily 8 | groups: 9 | gomod: 10 | update-types: 11 | - "patch" -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | 3 | on: 4 | workflow_call: 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | lint: 11 | name: lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-go@v5 16 | with: 17 | go-version: '1.22' 18 | cache: false 19 | - name: golangci-lint 20 | uses: golangci/golangci-lint-action@v4 21 | with: 22 | version: latest 23 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: write # needed to write releases 10 | id-token: write # needed for keyless signing 11 | 12 | jobs: 13 | lint: 14 | uses: ./.github/workflows/lint.yml 15 | test: 16 | needs: lint 17 | uses: ./.github/workflows/test.yml 18 | main: 19 | needs: [lint, test] 20 | strategy: 21 | matrix: 22 | go-version: [1.22.x] 23 | 24 | runs-on: ubuntu-latest 25 | 26 | steps: 27 | - name: Checkout code 28 | uses: actions/checkout@v4 29 | 30 | - name: Install Go 31 | uses: actions/setup-go@v4 32 | with: 33 | go-version: ${{ matrix.go-version }} 34 | 35 | - name: Install GoReleaser 36 | uses: goreleaser/goreleaser-action@v5 37 | with: 38 | install-only: true 39 | 40 | - name: Install ko 41 | uses: ko-build/setup-ko@v0.6 42 | 43 | - name: Login to Docker Hub 44 | uses: docker/login-action@v3 45 | with: 46 | username: ${{ secrets.DOCKERHUB_USER }} 47 | password: ${{ secrets.DOCKERHUB_SECRET }} 48 | 49 | # Push images to Dockerhub 50 | - name: Run Mage 51 | uses: magefile/mage-action@v3 52 | with: 53 | version: latest 54 | args: push:images 55 | -------------------------------------------------------------------------------- /.github/workflows/pr.yaml: -------------------------------------------------------------------------------- 1 | name: Pull Request 2 | 3 | on: 4 | pull_request: 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | lint: 11 | uses: ./.github/workflows/lint.yml 12 | test: 13 | needs: lint 14 | uses: ./.github/workflows/test.yml -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | permissions: 9 | contents: write # needed to write releases 10 | id-token: write # needed for keyless signing 11 | 12 | jobs: 13 | lint: 14 | uses: ./.github/workflows/lint.yml 15 | test: 16 | needs: lint 17 | uses: ./.github/workflows/test.yml 18 | release: 19 | needs: [lint, test] 20 | strategy: 21 | matrix: 22 | go-version: [1.22.x] 23 | 24 | runs-on: ubuntu-latest 25 | 26 | steps: 27 | - name: Checkout code 28 | uses: actions/checkout@v4 29 | 30 | - name: Install Go 31 | uses: actions/setup-go@v4 32 | with: 33 | go-version: ${{ matrix.go-version }} 34 | 35 | - name: Install GoReleaser 36 | uses: goreleaser/goreleaser-action@v5 37 | with: 38 | install-only: true 39 | 40 | # Push images to Dockerhub 41 | - name: Login to Docker Hub 42 | uses: docker/login-action@v3 43 | with: 44 | username: ${{ secrets.DOCKERHUB_USER }} 45 | password: ${{ secrets.DOCKERHUB_SECRET }} 46 | 47 | - name: Run Mage 48 | uses: magefile/mage-action@v3 49 | with: 50 | version: latest 51 | args: release:tag 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | workflow_call: 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | go-version: [1.22.x] 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v4 20 | 21 | - name: Install Go 22 | uses: actions/setup-go@v4 23 | with: 24 | go-version: ${{ matrix.go-version }} 25 | 26 | - name: Run Mage 27 | uses: magefile/mage-action@v3 28 | with: 29 | version: latest 30 | args: test 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | config.yaml 3 | **/config.yaml 4 | falco-talon 5 | dist/ 6 | Memory 7 | .idea/** 8 | tmp 9 | DO-NOT-COMMIT-local-setup.yaml 10 | deployment/compose/minio/data 11 | deployment/compose/tempo/data 12 | deployment/compose/loki/data 13 | .env 14 | pod*.yaml 15 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 5m 3 | modules-download-mode: readonly 4 | 5 | linters-settings: 6 | goconst: 7 | min-len: 2 8 | min-occurrences: 2 9 | gofmt: 10 | simplify: true 11 | goimports: 12 | local-prefixes: github.com/falcosecurity/falco-talon 13 | golint: 14 | min-confidence: 0 15 | govet: 16 | shadow: true 17 | enable-all: true 18 | misspell: 19 | locale: US 20 | 21 | linters: 22 | disable-all: true 23 | enable: 24 | - bodyclose 25 | - errcheck 26 | - goconst 27 | - gocritic 28 | - gofmt 29 | - goimports 30 | - revive 31 | - gosec 32 | - gosimple 33 | - govet 34 | - ineffassign 35 | - misspell 36 | - nakedret 37 | - staticcheck 38 | - stylecheck 39 | - typecheck 40 | - unconvert 41 | - unused 42 | - whitespace 43 | 44 | issues: 45 | exclude-rules: 46 | - path: server/manifest.go 47 | linters: 48 | - deadcode 49 | - unused 50 | - varcheck 51 | - path: server/configuration.go 52 | linters: 53 | - unused 54 | - path: _test.go 55 | linters: 56 | - bodyclose 57 | - goconst 58 | - scopelint # https://github.com/kyoh86/scopelint/issues/4 59 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: falco-talon 2 | 3 | env: 4 | - GO111MODULE=on 5 | - DOCKER_CLI_EXPERIMENTAL=enabled 6 | - DOCKER_BUILDKIT=1 7 | - BUILDX_PLATFORMS=linux/amd64,linux/arm64 8 | - COSIGN_YES=true 9 | 10 | snapshot: 11 | name_template: 'latest' 12 | 13 | checksum: 14 | name_template: 'checksums.txt' 15 | 16 | # Prevents parallel builds from stepping on each others toes downloading modules 17 | before: 18 | hooks: 19 | - go mod tidy 20 | - /bin/bash -c 'if [ -n "$(git --no-pager diff --exit-code go.mod go.sum)" ]; then exit 1; fi' 21 | 22 | builds: 23 | - id: "falco-talon" 24 | goos: 25 | - linux 26 | goarch: 27 | - amd64 28 | - arm64 29 | env: 30 | - CGO_ENABLED=0 31 | flags: 32 | - -trimpath 33 | ldflags: 34 | - "{{ .Env.LDFLAGS }}" 35 | binary: falco-talon 36 | 37 | kos: 38 | - id: falco-talon 39 | repository: falcosecurity/falco-talon 40 | tags: 41 | - '{{.Version}}' 42 | - latest 43 | bare: true 44 | preserve_import_paths: false 45 | platforms: 46 | - linux/amd64 47 | - linux/arm64 48 | 49 | release: 50 | github: 51 | owner: falcosecurity 52 | name: falco-talon 53 | prerelease: auto -------------------------------------------------------------------------------- /.ko.yaml: -------------------------------------------------------------------------------- 1 | defaultBaseImage: cgr.dev/chainguard/static 2 | defaultPlatforms: 3 | - linux/arm64 4 | - linux/amd64 5 | 6 | builds: 7 | - id: falco-talon 8 | dir: . 9 | main: . 10 | env: 11 | - CGO_ENABLED=0 12 | flags: 13 | - -trimpath 14 | ldflags: 15 | - -s -w 16 | - -extldflags "-static" 17 | - "{{ .Env.LDFLAGS }}" 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.3.0 - 2025-02-05 4 | 5 | - Add `kubernetes:sysdig` actionner 6 | 7 | ## 0.2.1 - 2024-12-09 8 | 9 | - Fix missing metrics for the `notifications` 10 | 11 | ## 0.2.0 - 2024-11-26 12 | 13 | - Add `gcp:function` actionner 14 | - Add `gcp:gcs` output 15 | - Add `ignore_standalone_pods` parameter for `kubernetes:terminate` actionner 16 | - Allow to wait until the completion of `kubernetes:drain` 17 | - Allow to check/print the rules without specifying a `config.yaml` 18 | - Migration of the Helm chart to [https://github.com/falcosecurity/charts/](https://github.com/falcosecurity/charts/) 19 | - Use smaller image for the `kubernetes:tcpdump` actionner 20 | 21 | ## 0.1.1 - 2024-10-01 22 | 23 | - Fix panics with actionners `cilium:networkpolicy` and `calico:networkpolicy` because of a wrong init 24 | - Fix panic when `output` is set in an `action` declared outside a `rule` 25 | - Fix errors with when the label key contains a `/` 26 | 27 | ## 0.1.0 - 2024-09-05 - *First GA release.* 28 | 29 | Official website for the docs: [https://docs.falco-talon.org/](https://docs.falco-talon.org/) 30 | 31 | This release contains: 32 | - the **rule engine to match the Falco events with the actions to perform** 33 | - basic **CLI** features: **check** of the rules, **list** the available actionners, outputs, notifiers, start the **web server** to receive the Falco events 34 | - **metrics** in **Prometheus** and **OTEL format** 35 | - export of **OTEL traces** 36 | - deduplication of the Falco events with **NATS Jetstream** 37 | - 13 **actionners**: 38 | - `kubernetes:terminate` 39 | - `kubernetes:label` 40 | - `kubernetes:networkpolicy` 41 | - `kubernetes:exec` 42 | - `kubernetes:script` 43 | - `kubernetes:log` 44 | - `kubernetes:delete` 45 | - `kubernetes:drain` 46 | - `kubernetes:download` 47 | - `kubernetes:tcpdump` 48 | - `aws:lambda` 49 | - `calico:networkpolicy` 50 | - `cilium:networkpolicy` 51 | - 6 **notifiers**: 52 | - `elasticsearch` 53 | - `k8s events` 54 | - `loki` 55 | - `slack` 56 | - `smtp` 57 | - `webhook` 58 | - 3 **outputs**: 59 | - `local:file` 60 | - `aws:s3` 61 | - `minio:s3` 62 | - 2 **context** enrichments: 63 | - `aws` (with IMDS) 64 | - `k8s` 65 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | approvers: 2 | - Issif 3 | - IgorEulalio 4 | - leogr 5 | - cpanato 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Falco Talon 2 | 3 | [![Falco Ecosystem Repository](https://raw.githubusercontent.com/falcosecurity/evolution/refs/heads/main/repos/badges/falco-ecosystem-blue.svg)](https://github.com/falcosecurity/evolution/blob/main/REPOSITORIES.md#ecosystem-scope) [![Incubating](https://img.shields.io/badge/status-incubating-orange?style=for-the-badge)](https://github.com/falcosecurity/evolution/blob/main/REPOSITORIES.md#incubating) 4 | [![Last Release](https://img.shields.io/github/v/release/falcosecurity/falco-talon?style=for-the-badge)](https://github.com/falcosecurity/falco-talon/releases/latest) 5 | 6 | ![last commit](https://img.shields.io/github/last-commit/falcosecurity/falco-talon?style=for-the-badge 7 | ) 8 | ![licence](https://img.shields.io/github/license/falcosecurity/falco-talon?style=for-the-badge 9 | ) 10 | ![docker pulls](https://img.shields.io/docker/pulls/falcosecurity/falco-talon?style=for-the-badge 11 | ) 12 | 13 | `Falco Talon` is a Response Engine for managing threats in your Kubernetes. It enhances the solutions proposed by the Falco community with a no-code tailor made solution. With easy rules, you can react to `events` from [`Falco`](https://falco.org) in milliseconds. 14 | 15 | - [Falco Talon](#falco-talon) 16 | - [Architecture](#architecture) 17 | - [Glossary](#glossary) 18 | - [Actionners](#actionners) 19 | - [Notifiers](#notifiers) 20 | - [Outputs](#outputs) 21 | - [Configuration](#configuration) 22 | - [Rules](#rules) 23 | - [Documentation](#documentation) 24 | - [Metrics](#metrics) 25 | - [Prometheus metrics](#prometheus-metrics) 26 | - [OTEL metrics](#otel-metrics) 27 | - [Traces](#traces) 28 | - [Docker images](#docker-images) 29 | - [Deployment](#deployment) 30 | - [Helm](#helm) 31 | - [Configure Falcosidekick](#configure-falcosidekick) 32 | - [Development](#development) 33 | - [License](#license) 34 | - [Author](#author) 35 | 36 | ## Architecture 37 | 38 | `Falco Talon` can receive the `events` from [`Falco`](https://falco.org) or [`Falcosidekick`](https://github.com/falcosecurity/falcosidekick): 39 | 40 | ```mermaid 41 | flowchart LR 42 | falco 43 | falcosidekick 44 | falco-talon 45 | falco -- event --> falcosidekick 46 | falco -- event --> falco-talon 47 | falcosidekick -- event --> falco-talon 48 | kubernetes -- context --> falco-talon 49 | falco-talon -- action --> aws 50 | falco-talon -- output --> minio 51 | falco-talon -- action --> kubernetes 52 | falco-talon -- notification --> slack 53 | ``` 54 | 55 | ### Glossary 56 | 57 | * `event`: an event detected by `Falco` and sent to its outputs 58 | * `rule`: defines criterias for linking the events with the actions to apply 59 | * `action`: each rule can sequentially run actions, each action refers to an actionner 60 | * `actionner`: defines what the action will do 61 | * `notifier`: defines what outputs to notify with the result of the action 62 | * `context`: composed of elements from the original Falco event, from Falco Talon and other sources, these elements can be used to dynamically configure the actions to perform. 63 | * `output`: defines the target where to store the resulting artifact that might has been created by an actionner 64 | 65 | ### Actionners 66 | 67 | The list of the available actionners can be found [HERE](https://docs.falco-talon.org/docs/actionners/list/). 68 | 69 | ### Notifiers 70 | 71 | The list of the available actionners can be found [HERE](https://docs.falco-talon.org/docs/notifiers/list/). 72 | 73 | ### Outputs 74 | 75 | The list of the available outputs can be found [HERE](https://docs.falco-talon.org/docs/outputs/list/). 76 | 77 | ### Configuration 78 | 79 | The static configuration of `Falco Talon` is set with a `.yaml` file (default: `./config.yaml`) or with environment variables. 80 | 81 | The list of the available settings can be found [HERE](https://docs.falco-talon.org/docs/configuration/). 82 | 83 | ### Rules 84 | 85 | You can find how to write your own rules [HERE](https://docs.falco-talon.org/docs/rules/). 86 | 87 | ## Documentation 88 | 89 | The full documentation is available on its own website: [https://docs.falco-talon.org/docs](https://docs.falco-talon.org/docs). 90 | 91 | ## Metrics 92 | 93 | ### Prometheus metrics 94 | 95 | The `/metrics` endpoint exposes some metrics in the Prometheus format. See [here](https://docs.falco-talon.org/docs/installation_usage/metrics#prometheus-metrics). 96 | 97 | ### OTEL metrics 98 | 99 | `Falco Talon` can export its metrics in the OTEL Metrics format. See [here](https://docs.falco-talon.org/docs/installation_usage/metrics#otel-metrics). 100 | 101 | ## Traces 102 | 103 | `Falco Talon` can export traces in the OTEL Traces format. See [here](https://docs.falco-talon.org/docs/installation_usage/traces). 104 | 105 | ## Docker images 106 | 107 | The docker images for `falco-talon` are built using [ko](https://github.com/google/ko) 108 | 109 | To generate the images to test locally you can run `mage buildImagesLocal` 110 | 111 | ## Deployment 112 | 113 | ### Helm 114 | 115 | The helm chart is available in the repo [`charts`](https://github.com/falcosecurity/charts/tree/master/charts/falco-talon). 116 | 117 | Two config files are provided: 118 | * `values.yaml` allows you to configure `Falcon Talon` and the deployment, the list of available values is [here](https://github.com/falcosecurity/charts/tree/master/charts/falco-talon#configuration) 119 | * `rules.yaml` contains rules to set 120 | 121 | ```shell 122 | helm repo add falcosecurity https://falcosecurity.github.io/charts 123 | helm repo update 124 | helm install falco-talon falcosecurity/falco -n falco --create-namespace 125 | ``` 126 | 127 | #### Configure Falcosidekick 128 | 129 | Once you have installed `Falco Talon` with Helm, you need to connect `Falcosidekick` by adding the flag `--set falcosidekick.config.talon.address=http://falco-talon:2803` 130 | 131 | ```shell 132 | helm install falco falcosecurity/falco --namespace falco \ 133 | --create-namespace \ 134 | --set tty=true \ 135 | --set falcosidekick.enabled=true \ 136 | --set falcosidekick.config.talon.address=http://falco-talon:2803 137 | ``` 138 | 139 | ## Development 140 | 141 | A [Docker Compose stack](./deployment/compose) is provided for local developments and tests. 142 | 143 | This stack starts these services: 144 | - **Grafana**: accessible at http://localhost:3000 145 | - **OTEL Collector** 146 | - **Prometheus**: accessible at http://localhost:9090 147 | - **Grafana Tempo** 148 | - **Minio**: accessible at http://localhost:9001 (root/changeme123) 149 | 150 | To start the stack: 151 | ```shell 152 | cd deployment/compose 153 | docker compose up -d 154 | ``` 155 | 156 | ## License 157 | 158 | Falco Talon is licensed to you under the [Apache 2.0](./LICENSE) open source license. 159 | 160 | ## Author 161 | 162 | Thomas Labarussias (https://github.com/Issif) 163 | 164 | -------------------------------------------------------------------------------- /actionners/aws/lambda/lambda.go: -------------------------------------------------------------------------------- 1 | package lambda 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net/http" 7 | 8 | "github.com/aws/aws-sdk-go-v2/service/lambda" 9 | "github.com/aws/aws-sdk-go-v2/service/lambda/types" 10 | 11 | awsChecks "github.com/falcosecurity/falco-talon/internal/aws/checks" 12 | aws "github.com/falcosecurity/falco-talon/internal/aws/client" 13 | "github.com/falcosecurity/falco-talon/internal/events" 14 | "github.com/falcosecurity/falco-talon/internal/models" 15 | "github.com/falcosecurity/falco-talon/internal/rules" 16 | "github.com/falcosecurity/falco-talon/utils" 17 | ) 18 | 19 | const ( 20 | Name string = "lambda" 21 | Category string = "aws" 22 | Description string = "Invoke an AWS lambda function forwarding the Falco event payload" 23 | Source string = "any" 24 | Continue bool = true 25 | UseContext bool = true 26 | AllowOutput bool = false 27 | RequireOutput bool = false 28 | Permissions string = `{ 29 | "Version": "2012-10-17", 30 | "Statement": [ 31 | { 32 | "Sid": "AllowInvokeLambdaFunction", 33 | "Effect": "Allow", 34 | "Action": "lambda:InvokeFunction", 35 | "Resource": "arn:aws:lambda:::function:" 36 | }, 37 | { 38 | "Sid": "AllowSTSGetCallerIdentity", 39 | "Effect": "Allow", 40 | "Action": "sts:GetCallerIdentity" 41 | } 42 | ] 43 | } 44 | ` 45 | Example string = `- action: Invoke Lambda function 46 | actionner: aws:lambda 47 | parameters: 48 | aws_lambda_name: sample-function 49 | aws_lambda_alias_or_version: $LATEST 50 | aws_lambda_invocation_type: RequestResponse 51 | ` 52 | ) 53 | 54 | var ( 55 | RequiredOutputFields = []string{} 56 | ) 57 | 58 | type Parameters struct { 59 | AWSLambdaName string `mapstructure:"aws_lambda_name" validate:"required"` 60 | AWSLambdaAliasOrVersion string `mapstructure:"aws_lambda_alias_or_version" validate:"omitempty"` 61 | AWSLambdaInvocationType string `mapstructure:"aws_lambda_invocation_type" validate:"omitempty,oneof=RequestResponse Event DryRun"` 62 | } 63 | 64 | type Actionner struct{} 65 | 66 | func Register() *Actionner { 67 | return new(Actionner) 68 | } 69 | 70 | func (a Actionner) Init() error { 71 | return aws.Init() 72 | } 73 | 74 | func (a Actionner) Information() models.Information { 75 | return models.Information{ 76 | Name: Name, 77 | FullName: Category + ":" + Name, 78 | Category: Category, 79 | Description: Description, 80 | Source: Source, 81 | RequiredOutputFields: RequiredOutputFields, 82 | Permissions: Permissions, 83 | Example: Example, 84 | Continue: Continue, 85 | AllowOutput: AllowOutput, 86 | RequireOutput: RequireOutput, 87 | } 88 | } 89 | 90 | func (a Actionner) Parameters() models.Parameters { 91 | return Parameters{ 92 | AWSLambdaName: "", 93 | AWSLambdaAliasOrVersion: "$LATEST", 94 | AWSLambdaInvocationType: "RequestResponse", 95 | } 96 | } 97 | 98 | func (a Actionner) Checks(_ *events.Event, action *rules.Action) error { 99 | var parameters Parameters 100 | err := utils.DecodeParams(action.GetParameters(), ¶meters) 101 | if err != nil { 102 | return err 103 | } 104 | return awsChecks.CheckLambdaExist.Run(awsChecks.CheckLambdaExist{}, parameters.AWSLambdaName) 105 | } 106 | 107 | func (a Actionner) Run(event *events.Event, action *rules.Action) (utils.LogLine, *models.Data, error) { 108 | lambdaClient := aws.GetLambdaClient() 109 | 110 | var parameters Parameters 111 | err := utils.DecodeParams(action.GetParameters(), ¶meters) 112 | if err != nil { 113 | return utils.LogLine{ 114 | Objects: nil, 115 | Error: err.Error(), 116 | Status: utils.FailureStr, 117 | }, nil, err 118 | } 119 | 120 | objects := map[string]string{ 121 | "name": parameters.AWSLambdaName, 122 | "version": parameters.AWSLambdaAliasOrVersion, 123 | } 124 | 125 | payload, err := json.Marshal(event) 126 | if err != nil { 127 | return utils.LogLine{ 128 | Objects: objects, 129 | Error: err.Error(), 130 | Status: utils.FailureStr, 131 | }, nil, err 132 | } 133 | 134 | input := &lambda.InvokeInput{ 135 | FunctionName: ¶meters.AWSLambdaName, 136 | ClientContext: nil, 137 | InvocationType: getInvocationType(parameters.AWSLambdaInvocationType), 138 | Payload: payload, 139 | Qualifier: getLambdaVersion(¶meters.AWSLambdaAliasOrVersion), 140 | } 141 | 142 | lambdaOutput, err := lambdaClient.Invoke(context.Background(), input) 143 | if err != nil { 144 | return utils.LogLine{ 145 | Objects: objects, 146 | Error: err.Error(), 147 | Status: utils.FailureStr, 148 | }, nil, err 149 | } 150 | 151 | status := utils.SuccessStr 152 | if lambdaOutput.StatusCode != http.StatusOK && lambdaOutput.StatusCode != http.StatusNoContent { 153 | status = utils.FailureStr 154 | } 155 | return utils.LogLine{ 156 | Objects: objects, 157 | Output: string(lambdaOutput.Payload), 158 | Status: status, 159 | }, nil, nil 160 | } 161 | 162 | func (a Actionner) CheckParameters(action *rules.Action) error { 163 | var parameters Parameters 164 | err := utils.DecodeParams(action.GetParameters(), ¶meters) 165 | if err != nil { 166 | return err 167 | } 168 | 169 | err = utils.ValidateStruct(parameters) 170 | if err != nil { 171 | return err 172 | } 173 | return nil 174 | } 175 | 176 | func getInvocationType(invocationType string) types.InvocationType { 177 | switch invocationType { 178 | case "RequestResponse": 179 | return types.InvocationTypeRequestResponse 180 | case "Event": 181 | return types.InvocationTypeEvent 182 | case "DryRun": 183 | return types.InvocationTypeDryRun 184 | default: 185 | return types.InvocationTypeRequestResponse // Default 186 | } 187 | } 188 | 189 | func getLambdaVersion(qualifier *string) *string { 190 | if qualifier == nil || *qualifier == "" { 191 | defaultVal := "$LATEST" 192 | return &defaultVal 193 | } 194 | return qualifier 195 | } 196 | -------------------------------------------------------------------------------- /actionners/kubernetes/cordon/cordon.go: -------------------------------------------------------------------------------- 1 | package cordon 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/apimachinery/pkg/types" 9 | 10 | "github.com/falcosecurity/falco-talon/internal/events" 11 | k8sChecks "github.com/falcosecurity/falco-talon/internal/kubernetes/checks" 12 | k8s "github.com/falcosecurity/falco-talon/internal/kubernetes/client" 13 | "github.com/falcosecurity/falco-talon/internal/models" 14 | "github.com/falcosecurity/falco-talon/internal/rules" 15 | "github.com/falcosecurity/falco-talon/utils" 16 | ) 17 | 18 | const ( 19 | Name string = "cordon" 20 | Category string = "kubernetes" 21 | Description string = "Cordon a node" 22 | Source string = "syscalls, k8s_audit" 23 | Continue bool = true 24 | UseContext bool = false 25 | AllowOutput bool = false 26 | RequireOutput bool = false 27 | Permissions string = `apiVersion: rbac.authorization.k8s.io/v1 28 | kind: ClusterRole 29 | metadata: 30 | name: falco-talon 31 | rules: 32 | - apiGroups: 33 | - "" 34 | resources: 35 | - pods 36 | verbs: 37 | - get 38 | - list 39 | - apiGroups: 40 | - "" 41 | resources: 42 | - nodes 43 | verbs: 44 | - get 45 | - update 46 | - patch 47 | ` 48 | Example string = `- action: Exec a command into the pod 49 | actionner: kubernetes:exec 50 | parameters: 51 | shell: /bin/bash 52 | command: "cat ${FD_NAME}" 53 | ` 54 | ) 55 | 56 | var ( 57 | RequiredOutputFields = []string{"k8s.ns.name", "k8s.pod.name"} 58 | ) 59 | 60 | const ( 61 | jsonPatch = `[{"op": "replace", "path": "/spec/unschedulable", "value": true}]` 62 | ) 63 | 64 | type Actionner struct{} 65 | 66 | func Register() *Actionner { 67 | return new(Actionner) 68 | } 69 | 70 | func (a Actionner) Init() error { 71 | return k8s.Init() 72 | } 73 | 74 | func (a Actionner) Information() models.Information { 75 | return models.Information{ 76 | Name: Name, 77 | FullName: Category + ":" + Name, 78 | Category: Category, 79 | Description: Description, 80 | Source: Source, 81 | RequiredOutputFields: RequiredOutputFields, 82 | Permissions: Permissions, 83 | Example: Example, 84 | Continue: Continue, 85 | AllowOutput: AllowOutput, 86 | RequireOutput: RequireOutput, 87 | } 88 | } 89 | func (a Actionner) Parameters() models.Parameters { 90 | return nil 91 | } 92 | 93 | func (a Actionner) Checks(event *events.Event, _ *rules.Action) error { 94 | return k8sChecks.CheckPodExist(event) 95 | } 96 | 97 | func (a Actionner) Run(event *events.Event, _ *rules.Action) (utils.LogLine, *models.Data, error) { 98 | podName := event.GetPodName() 99 | namespace := event.GetNamespaceName() 100 | 101 | objects := map[string]string{} 102 | 103 | client := k8s.GetClient() 104 | 105 | pod, err := client.GetPod(podName, namespace) 106 | if err != nil { 107 | objects["pod"] = podName 108 | objects["namespace"] = namespace 109 | return utils.LogLine{ 110 | Objects: objects, 111 | Error: err.Error(), 112 | Status: utils.FailureStr, 113 | }, nil, err 114 | } 115 | 116 | node, err := client.GetNodeFromPod(pod) 117 | if err != nil { 118 | return utils.LogLine{ 119 | Objects: objects, 120 | Error: err.Error(), 121 | Status: utils.FailureStr, 122 | }, nil, err 123 | } 124 | 125 | objects["node"] = node.Name 126 | 127 | _, err = client.Clientset.CoreV1().Nodes().Patch(context.Background(), node.Name, types.JSONPatchType, []byte(jsonPatch), metav1.PatchOptions{}) 128 | if err != nil { 129 | return utils.LogLine{ 130 | Objects: objects, 131 | Error: err.Error(), 132 | Status: utils.FailureStr, 133 | }, nil, err 134 | } 135 | 136 | return utils.LogLine{ 137 | Objects: objects, 138 | Output: fmt.Sprintf("the node '%v' has been cordoned", node.Name), 139 | Status: utils.SuccessStr, 140 | }, nil, nil 141 | } 142 | 143 | func (a Actionner) CheckParameters(_ *rules.Action) error { return nil } 144 | -------------------------------------------------------------------------------- /actionners/kubernetes/delete/delete.go: -------------------------------------------------------------------------------- 1 | package networkpolicy 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | 10 | "github.com/falcosecurity/falco-talon/internal/events" 11 | k8sChecks "github.com/falcosecurity/falco-talon/internal/kubernetes/checks" 12 | k8s "github.com/falcosecurity/falco-talon/internal/kubernetes/client" 13 | "github.com/falcosecurity/falco-talon/internal/models" 14 | "github.com/falcosecurity/falco-talon/internal/rules" 15 | "github.com/falcosecurity/falco-talon/utils" 16 | ) 17 | 18 | const ( 19 | Name string = "delete" 20 | Category string = "kubernetes" 21 | Description string = "Delete a resource" 22 | Source string = "k8s_audit" 23 | Continue bool = false 24 | UseContext bool = false 25 | AllowOutput bool = false 26 | RequireOutput bool = false 27 | Permissions string = `apiVersion: rbac.authorization.k8s.io/v1 28 | kind: ClusterRole 29 | metadata: 30 | name: falco-talon 31 | rules: 32 | - apiGroups: 33 | - "" 34 | resources: 35 | - namespaces 36 | verbs: 37 | - get 38 | - delete 39 | - apiGroups: 40 | - "" 41 | resources: 42 | - pods 43 | verbs: 44 | - get 45 | - delete 46 | - list 47 | - apiGroups: 48 | - apps 49 | resources: 50 | - daemonsets 51 | verbs: 52 | - get 53 | - delete 54 | - apiGroups: 55 | - apps 56 | resources: 57 | - deployments 58 | verbs: 59 | - get 60 | - delete 61 | - apiGroups: 62 | - apps 63 | resources: 64 | - replicasets 65 | verbs: 66 | - get 67 | - delete 68 | - apiGroups: 69 | - apps 70 | resources: 71 | - statefulsets 72 | verbs: 73 | - get 74 | - delete 75 | - apiGroups: 76 | - rbac.authorization.k8s.io 77 | resources: 78 | - roles 79 | verbs: 80 | - get 81 | - delete 82 | - apiGroups: 83 | - rbac.authorization.k8s.io 84 | resources: 85 | - clusterroles 86 | verbs: 87 | - get 88 | - delete 89 | - apiGroups: 90 | - "" 91 | resources: 92 | - configmaps 93 | verbs: 94 | - get 95 | - delete 96 | - apiGroups: 97 | - "" 98 | resources: 99 | - secrets 100 | verbs: 101 | - get 102 | - delete 103 | ` 104 | Example string = `- action: Delete the suspicious resource 105 | actionner: kubernetes:delete 106 | ` 107 | ) 108 | 109 | const namespaces string = "namespaces" 110 | 111 | var ( 112 | RequiredOutputFields = []string{"ka.target.resource", "ka.target.name", "ka.target.namespace"} 113 | ) 114 | 115 | type Actionner struct{} 116 | 117 | func Register() *Actionner { 118 | return new(Actionner) 119 | } 120 | 121 | func (a Actionner) Init() error { 122 | return k8s.Init() 123 | } 124 | 125 | func (a Actionner) Information() models.Information { 126 | return models.Information{ 127 | Name: Name, 128 | FullName: Category + ":" + Name, 129 | Category: Category, 130 | Description: Description, 131 | Source: Source, 132 | RequiredOutputFields: RequiredOutputFields, 133 | Permissions: Permissions, 134 | Example: Example, 135 | Continue: Continue, 136 | AllowOutput: AllowOutput, 137 | RequireOutput: RequireOutput, 138 | } 139 | } 140 | func (a Actionner) Parameters() models.Parameters { 141 | return nil 142 | } 143 | 144 | func (a Actionner) Checks(event *events.Event, _ *rules.Action) error { 145 | return k8sChecks.CheckTargetExist(event) 146 | } 147 | 148 | func (a Actionner) Run(event *events.Event, _ *rules.Action) (utils.LogLine, *models.Data, error) { 149 | name := event.GetTargetName() 150 | resource := event.GetTargetResource() 151 | namespace := event.GetTargetNamespace() 152 | 153 | objects := map[string]string{ 154 | "name": name, 155 | "resource": resource, 156 | "namespace": namespace, 157 | } 158 | 159 | client := k8s.GetClient() 160 | 161 | var err error 162 | 163 | switch resource { 164 | case namespaces: 165 | err = client.Clientset.CoreV1().Namespaces().Delete(context.Background(), name, metav1.DeleteOptions{}) 166 | case "configmaps": 167 | err = client.Clientset.CoreV1().ConfigMaps(namespace).Delete(context.Background(), name, metav1.DeleteOptions{}) 168 | case "secrets": 169 | err = client.Clientset.CoreV1().Secrets(namespace).Delete(context.Background(), name, metav1.DeleteOptions{}) 170 | case "deployments": 171 | err = client.Clientset.AppsV1().Deployments(namespace).Delete(context.Background(), name, metav1.DeleteOptions{}) 172 | case "daemonsets": 173 | err = client.Clientset.AppsV1().DaemonSets(namespace).Delete(context.Background(), name, metav1.DeleteOptions{}) 174 | case "statefulsets": 175 | err = client.Clientset.AppsV1().StatefulSets(namespace).Delete(context.Background(), name, metav1.DeleteOptions{}) 176 | case "replicasets": 177 | err = client.Clientset.AppsV1().ReplicaSets(namespace).Delete(context.Background(), name, metav1.DeleteOptions{}) 178 | case "services": 179 | err = client.Clientset.CoreV1().Services(namespace).Delete(context.Background(), name, metav1.DeleteOptions{}) 180 | case "serviceaccounts": 181 | err = client.Clientset.CoreV1().ServiceAccounts(namespace).Delete(context.Background(), name, metav1.DeleteOptions{}) 182 | case "roles": 183 | err = client.Clientset.RbacV1().Roles(namespace).Delete(context.Background(), name, metav1.DeleteOptions{}) 184 | case "clusterroles": 185 | err = client.Clientset.RbacV1().ClusterRoles().Delete(context.Background(), name, metav1.DeleteOptions{}) 186 | } 187 | 188 | if err != nil { 189 | return utils.LogLine{ 190 | Objects: objects, 191 | Error: err.Error(), 192 | Status: utils.FailureStr, 193 | }, nil, err 194 | } 195 | 196 | var output string 197 | if resource == namespaces { 198 | output = fmt.Sprintf("the %v '%v' has been deleted", strings.TrimSuffix(resource, "s"), name) 199 | } else { 200 | output = fmt.Sprintf("the %v '%v' in the namespace '%v' has been deleted", strings.TrimSuffix(resource, "s"), name, namespace) 201 | } 202 | 203 | return utils.LogLine{ 204 | Objects: objects, 205 | Output: output, 206 | Status: utils.SuccessStr, 207 | }, nil, nil 208 | } 209 | 210 | func (a Actionner) CheckParameters(_ *rules.Action) error { return nil } 211 | -------------------------------------------------------------------------------- /actionners/kubernetes/download/download.go: -------------------------------------------------------------------------------- 1 | package copy 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/falcosecurity/falco-talon/internal/events" 9 | k8sChecks "github.com/falcosecurity/falco-talon/internal/kubernetes/checks" 10 | k8s "github.com/falcosecurity/falco-talon/internal/kubernetes/client" 11 | "github.com/falcosecurity/falco-talon/internal/models" 12 | "github.com/falcosecurity/falco-talon/internal/rules" 13 | "github.com/falcosecurity/falco-talon/utils" 14 | ) 15 | 16 | const ( 17 | Name string = "download" 18 | Category string = "kubernetes" 19 | Description string = "Download a file from a pod" 20 | Source string = "syscalls" 21 | Continue bool = true 22 | UseContext bool = false 23 | AllowOutput bool = false 24 | RequireOutput bool = true 25 | Permissions string = `apiVersion: rbac.authorization.k8s.io/v1 26 | kind: ClusterRole 27 | metadata: 28 | name: falco-talon 29 | rules: 30 | - apiGroups: 31 | - "" 32 | resources: 33 | - pods 34 | verbs: 35 | - get 36 | - list 37 | - apiGroups: 38 | - "" 39 | resources: 40 | - pods/exec 41 | verbs: 42 | - get 43 | - create 44 | ` 45 | Example string = `- action: Get logs of the pod 46 | actionner: kubernetes:download 47 | parameters: 48 | tail_lines: 200 49 | output: 50 | target: aws:s3 51 | parameters: 52 | bucket: my-bucket 53 | prefix: /files/ 54 | ` 55 | ) 56 | 57 | var ( 58 | RequiredOutputFields = []string{"k8s.ns.name", "k8s.pod.name"} 59 | ) 60 | 61 | type Parameters struct { 62 | File string `mapstructure:"file" validate:"required"` 63 | } 64 | 65 | type Actionner struct{} 66 | 67 | func Register() *Actionner { 68 | return new(Actionner) 69 | } 70 | 71 | func (a Actionner) Init() error { 72 | return k8s.Init() 73 | } 74 | 75 | func (a Actionner) Information() models.Information { 76 | return models.Information{ 77 | Name: Name, 78 | FullName: Category + ":" + Name, 79 | Category: Category, 80 | Description: Description, 81 | Source: Source, 82 | RequiredOutputFields: RequiredOutputFields, 83 | Permissions: Permissions, 84 | Example: Example, 85 | Continue: Continue, 86 | AllowOutput: AllowOutput, 87 | RequireOutput: RequireOutput, 88 | } 89 | } 90 | func (a Actionner) Parameters() models.Parameters { 91 | return Parameters{ 92 | File: "", 93 | } 94 | } 95 | 96 | func (a Actionner) Checks(event *events.Event, _ *rules.Action) error { 97 | return k8sChecks.CheckPodExist(event) 98 | } 99 | 100 | func (a Actionner) Run(event *events.Event, action *rules.Action) (utils.LogLine, *models.Data, error) { 101 | pod := event.GetPodName() 102 | namespace := event.GetNamespaceName() 103 | 104 | objects := map[string]string{ 105 | "pod": pod, 106 | "namespace": namespace, 107 | } 108 | 109 | var parameters Parameters 110 | err := utils.DecodeParams(action.GetParameters(), ¶meters) 111 | if err != nil { 112 | return utils.LogLine{ 113 | Objects: nil, 114 | Error: err.Error(), 115 | Status: utils.FailureStr, 116 | }, nil, err 117 | } 118 | 119 | file := new(string) 120 | *file = parameters.File 121 | 122 | event.ExportEnvVars() 123 | *file = os.ExpandEnv(*file) 124 | 125 | objects["file"] = *file 126 | 127 | client := k8s.GetClient() 128 | 129 | p, _ := client.GetPod(pod, namespace) 130 | containers := k8s.GetContainers(p) 131 | if len(containers) == 0 { 132 | err = fmt.Errorf("no container found") 133 | return utils.LogLine{ 134 | Objects: objects, 135 | Error: err.Error(), 136 | Status: utils.FailureStr, 137 | }, nil, err 138 | } 139 | 140 | output := new(bytes.Buffer) 141 | for i, container := range containers { 142 | command := []string{"cat", *file} 143 | output, err = client.Exec(namespace, pod, container, command, "") 144 | if err != nil { 145 | if i == len(containers)-1 { 146 | return utils.LogLine{ 147 | Objects: objects, 148 | Error: err.Error(), 149 | Status: utils.FailureStr, 150 | }, nil, err 151 | } 152 | continue 153 | } 154 | break 155 | } 156 | 157 | return utils.LogLine{ 158 | Objects: objects, 159 | Output: fmt.Sprintf("the file '%v' has been downloaded", *file), 160 | Status: utils.SuccessStr, 161 | }, &models.Data{Name: *file, Objects: objects, Bytes: output.Bytes()}, nil 162 | } 163 | 164 | func (a Actionner) CheckParameters(action *rules.Action) error { 165 | var parameters Parameters 166 | 167 | err := utils.DecodeParams(action.GetParameters(), ¶meters) 168 | if err != nil { 169 | return err 170 | } 171 | 172 | err = utils.ValidateStruct(parameters) 173 | if err != nil { 174 | return err 175 | } 176 | 177 | return nil 178 | } 179 | -------------------------------------------------------------------------------- /actionners/kubernetes/exec/exec.go: -------------------------------------------------------------------------------- 1 | package exec 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/falcosecurity/falco-talon/internal/events" 9 | k8sChecks "github.com/falcosecurity/falco-talon/internal/kubernetes/checks" 10 | k8s "github.com/falcosecurity/falco-talon/internal/kubernetes/client" 11 | "github.com/falcosecurity/falco-talon/internal/models" 12 | "github.com/falcosecurity/falco-talon/internal/rules" 13 | "github.com/falcosecurity/falco-talon/utils" 14 | ) 15 | 16 | const ( 17 | Name string = "exec" 18 | Category string = "kubernetes" 19 | Description string = "Exec a command in a pod" 20 | Source string = "syscalls, k8s_audit" 21 | Continue bool = true 22 | UseContext bool = true 23 | AllowOutput bool = false 24 | RequireOutput bool = false 25 | Permissions string = `apiVersion: rbac.authorization.k8s.io/v1 26 | kind: ClusterRole 27 | metadata: 28 | name: falco-talon 29 | rules: 30 | - apiGroups: 31 | - "" 32 | resources: 33 | - pods 34 | verbs: 35 | - get 36 | - list 37 | - apiGroups: 38 | - "" 39 | resources: 40 | - pods/exec 41 | verbs: 42 | - get 43 | - create 44 | ` 45 | Example string = `- action: Exec a command into the pod 46 | actionner: kubernetes:exec 47 | parameters: 48 | shell: /bin/bash 49 | command: "cat ${FD_NAME}" 50 | ` 51 | ) 52 | 53 | var ( 54 | RequiredOutputFields = []string{"k8s.ns.name", "k8s.pod.name"} 55 | ) 56 | 57 | type Parameters struct { 58 | Command string `mapstructure:"command" validate:"required"` 59 | Shell string `mapstructure:"shell" validate:"omitempty"` 60 | } 61 | 62 | type Actionner struct{} 63 | 64 | func Register() *Actionner { 65 | return new(Actionner) 66 | } 67 | 68 | func (a Actionner) Init() error { 69 | return k8s.Init() 70 | } 71 | 72 | func (a Actionner) Information() models.Information { 73 | return models.Information{ 74 | Name: Name, 75 | FullName: Category + ":" + Name, 76 | Category: Category, 77 | Description: Description, 78 | Source: Source, 79 | RequiredOutputFields: RequiredOutputFields, 80 | Permissions: Permissions, 81 | Example: Example, 82 | Continue: Continue, 83 | AllowOutput: AllowOutput, 84 | RequireOutput: RequireOutput, 85 | } 86 | } 87 | func (a Actionner) Parameters() models.Parameters { 88 | return Parameters{ 89 | Command: "", 90 | Shell: "/bin/sh", 91 | } 92 | } 93 | 94 | func (a Actionner) Checks(event *events.Event, _ *rules.Action) error { 95 | return k8sChecks.CheckPodExist(event) 96 | } 97 | 98 | func (a Actionner) Run(event *events.Event, action *rules.Action) (utils.LogLine, *models.Data, error) { 99 | pod := event.GetPodName() 100 | namespace := event.GetNamespaceName() 101 | 102 | objects := map[string]string{ 103 | "pod": pod, 104 | "namespace": namespace, 105 | } 106 | 107 | var parameters Parameters 108 | err := utils.DecodeParams(action.GetParameters(), ¶meters) 109 | if err != nil { 110 | return utils.LogLine{ 111 | Objects: nil, 112 | Error: err.Error(), 113 | Status: utils.FailureStr, 114 | }, nil, err 115 | } 116 | 117 | shell := new(string) 118 | if parameters.Shell != "" { 119 | *shell = parameters.Shell 120 | } else { 121 | *shell = "/bin/sh" 122 | } 123 | 124 | command := new(string) 125 | *command = parameters.Command 126 | 127 | event.ExportEnvVars() 128 | *command = os.ExpandEnv(*command) 129 | 130 | client := k8s.GetClient() 131 | 132 | p, _ := client.GetPod(pod, namespace) 133 | containers := k8s.GetContainers(p) 134 | if len(containers) == 0 { 135 | err = fmt.Errorf("no container found") 136 | return utils.LogLine{ 137 | Objects: objects, 138 | Error: err.Error(), 139 | Status: utils.FailureStr, 140 | }, nil, err 141 | } 142 | 143 | output := new(bytes.Buffer) 144 | for i, container := range containers { 145 | command := []string{*shell, "-c", *command} 146 | output, err = client.Exec(namespace, pod, container, command, "") 147 | if err != nil { 148 | if i == len(containers)-1 { 149 | return utils.LogLine{ 150 | Objects: objects, 151 | Error: err.Error(), 152 | Status: utils.FailureStr, 153 | }, nil, err 154 | } 155 | continue 156 | } 157 | } 158 | 159 | return utils.LogLine{ 160 | Objects: objects, 161 | Output: utils.RemoveAnsiCharacters(output.String()), 162 | Status: utils.SuccessStr, 163 | }, nil, nil 164 | } 165 | 166 | func (a Actionner) CheckParameters(action *rules.Action) error { 167 | var parameters Parameters 168 | err := utils.DecodeParams(action.GetParameters(), ¶meters) 169 | if err != nil { 170 | return err 171 | } 172 | 173 | err = utils.ValidateStruct(parameters) 174 | if err != nil { 175 | return err 176 | } 177 | 178 | return nil 179 | } 180 | -------------------------------------------------------------------------------- /actionners/kubernetes/helpers/helpers.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/go-playground/validator/v10" 9 | ) 10 | 11 | const ValidatorMinHealthyReplicas = "is_absolut_or_percent" 12 | 13 | func ValidateMinHealthyReplicas(fl validator.FieldLevel) bool { 14 | minHealthyReplicas := fl.Field().String() 15 | reg := regexp.MustCompile(`\d+(%)?`) 16 | if !reg.MatchString(minHealthyReplicas) { 17 | return false 18 | } 19 | if strings.HasSuffix(minHealthyReplicas, "%") { 20 | percent, err := strconv.ParseInt(strings.TrimSuffix(minHealthyReplicas, "%"), 10, 64) 21 | if err != nil { 22 | return false 23 | } 24 | if percent < 0 || percent > 100 { 25 | return false 26 | } 27 | } else { 28 | absolut, err := strconv.ParseInt(minHealthyReplicas, 10, 64) 29 | if err != nil { 30 | return false 31 | } 32 | if absolut < 0 { 33 | return false 34 | } 35 | } 36 | return true 37 | } 38 | 39 | // ParseMinHealthyReplicas returns an integer for the value and a string for the type ("percent" or "absolut") 40 | func ParseMinHealthyReplicas(value string) (int64, string, error) { 41 | if strings.HasSuffix(value, "%") { 42 | percent, err := strconv.ParseInt(strings.TrimSuffix(value, "%"), 10, 64) 43 | if err != nil { 44 | return 0, "", err 45 | } 46 | return percent, "percent", nil 47 | } 48 | absolut, err := strconv.ParseInt(value, 10, 64) 49 | if err != nil { 50 | return 0, "", err 51 | } 52 | return absolut, "absolut", nil 53 | } 54 | -------------------------------------------------------------------------------- /actionners/kubernetes/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | 9 | corev1 "k8s.io/api/core/v1" 10 | 11 | "github.com/falcosecurity/falco-talon/internal/events" 12 | k8sChecks "github.com/falcosecurity/falco-talon/internal/kubernetes/checks" 13 | k8s "github.com/falcosecurity/falco-talon/internal/kubernetes/client" 14 | "github.com/falcosecurity/falco-talon/internal/models" 15 | "github.com/falcosecurity/falco-talon/internal/rules" 16 | "github.com/falcosecurity/falco-talon/utils" 17 | ) 18 | 19 | const ( 20 | Name string = "log" 21 | Category string = "kubernetes" 22 | Description string = "Get logs from a pod" 23 | Source string = "syscalls, k8s_audit" 24 | Continue bool = true 25 | UseContext bool = false 26 | AllowOutput bool = true 27 | RequireOutput bool = false 28 | Permissions string = `apiVersion: rbac.authorization.k8s.io/v1 29 | kind: ClusterRole 30 | metadata: 31 | name: falco-talon 32 | rules: 33 | - apiGroups: 34 | - "" 35 | resources: 36 | - pods 37 | verbs: 38 | - get 39 | - list 40 | - apiGroups: 41 | - "" 42 | resources: 43 | - pods/log 44 | verbs: 45 | - get 46 | ` 47 | Example string = `- action: Get logs of the pod 48 | actionner: kubernetes:log 49 | parameters: 50 | tail_lines: 200 51 | output: 52 | target: aws:s3 53 | parameters: 54 | bucket: my-bucket 55 | prefix: /logs/ 56 | ` 57 | ) 58 | 59 | const ( 60 | defaultTailLines int = 20 61 | ) 62 | 63 | var ( 64 | RequiredOutputFields = []string{"k8s.ns.name", "k8s.pod.name"} 65 | ) 66 | 67 | type Parameters struct { 68 | TailLines int `mapstructure:"tail_lines" validate:"gte=0,omitempty"` 69 | } 70 | 71 | type Actionner struct{} 72 | 73 | func Register() *Actionner { 74 | return new(Actionner) 75 | } 76 | 77 | func (a Actionner) Init() error { 78 | return k8s.Init() 79 | } 80 | 81 | func (a Actionner) Information() models.Information { 82 | return models.Information{ 83 | Name: Name, 84 | FullName: Category + ":" + Name, 85 | Category: Category, 86 | Description: Description, 87 | Source: Source, 88 | RequiredOutputFields: RequiredOutputFields, 89 | Permissions: Permissions, 90 | Example: Example, 91 | Continue: Continue, 92 | AllowOutput: AllowOutput, 93 | RequireOutput: RequireOutput, 94 | } 95 | } 96 | func (a Actionner) Parameters() models.Parameters { 97 | return Parameters{ 98 | TailLines: 20, 99 | } 100 | } 101 | 102 | func (a Actionner) Checks(event *events.Event, _ *rules.Action) error { 103 | return k8sChecks.CheckPodExist(event) 104 | } 105 | 106 | func (a Actionner) Run(event *events.Event, action *rules.Action) (utils.LogLine, *models.Data, error) { 107 | pod := event.GetPodName() 108 | namespace := event.GetNamespaceName() 109 | 110 | objects := map[string]string{ 111 | "pod": pod, 112 | "namespace": namespace, 113 | } 114 | 115 | var parameters Parameters 116 | err := utils.DecodeParams(action.GetParameters(), ¶meters) 117 | if err != nil { 118 | return utils.LogLine{ 119 | Objects: nil, 120 | Error: err.Error(), 121 | Status: utils.FailureStr, 122 | }, nil, err 123 | } 124 | 125 | tailLines := new(int64) 126 | *tailLines = int64(defaultTailLines) 127 | if parameters.TailLines > 0 { 128 | *tailLines = int64(parameters.TailLines) 129 | } 130 | 131 | client := k8s.GetClient() 132 | 133 | p, _ := client.GetPod(pod, namespace) 134 | containers := k8s.GetContainers(p) 135 | if len(containers) == 0 { 136 | err := fmt.Errorf("no container found") 137 | return utils.LogLine{ 138 | Objects: objects, 139 | Error: err.Error(), 140 | Status: utils.FailureStr, 141 | }, nil, err 142 | } 143 | 144 | ctx := context.Background() 145 | var output []byte 146 | 147 | for i, container := range containers { 148 | logs, err := client.Clientset.CoreV1().Pods(namespace).GetLogs(pod, &corev1.PodLogOptions{ 149 | Container: container, 150 | TailLines: tailLines, 151 | }).Stream(ctx) 152 | if err != nil { 153 | if i == len(containers)-1 { 154 | return utils.LogLine{ 155 | Objects: objects, 156 | Error: err.Error(), 157 | Status: utils.FailureStr, 158 | }, nil, err 159 | } 160 | continue 161 | } 162 | defer logs.Close() 163 | 164 | buf := new(bytes.Buffer) 165 | _, err = io.Copy(buf, logs) 166 | if err != nil { 167 | return utils.LogLine{ 168 | Objects: objects, 169 | Status: utils.FailureStr, 170 | Error: err.Error(), 171 | }, nil, err 172 | } 173 | 174 | output = buf.Bytes() 175 | if len(output) != 0 { 176 | break 177 | } 178 | } 179 | 180 | return utils.LogLine{ 181 | Objects: objects, 182 | Output: fmt.Sprintf("the logs for the pod '%v' in the namespace '%v' has been downloaded", pod, namespace), 183 | Status: utils.SuccessStr, 184 | }, &models.Data{Name: "log", Objects: objects, Bytes: output}, nil 185 | } 186 | 187 | func (a Actionner) CheckParameters(action *rules.Action) error { 188 | var parameters Parameters 189 | 190 | err := utils.DecodeParams(action.GetParameters(), ¶meters) 191 | if err != nil { 192 | return err 193 | } 194 | 195 | err = utils.ValidateStruct(parameters) 196 | if err != nil { 197 | return err 198 | } 199 | 200 | return nil 201 | } 202 | -------------------------------------------------------------------------------- /actionners/kubernetes/script/script.go: -------------------------------------------------------------------------------- 1 | package script 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/falcosecurity/falco-talon/internal/events" 10 | k8sChecks "github.com/falcosecurity/falco-talon/internal/kubernetes/checks" 11 | k8s "github.com/falcosecurity/falco-talon/internal/kubernetes/client" 12 | "github.com/falcosecurity/falco-talon/internal/models" 13 | "github.com/falcosecurity/falco-talon/internal/rules" 14 | "github.com/falcosecurity/falco-talon/utils" 15 | ) 16 | 17 | const ( 18 | Name string = "script" 19 | Category string = "kubernetes" 20 | Description string = "Run a script in a pod" 21 | Source string = "syscalls, k8s_audit" 22 | Continue bool = true 23 | UseContext bool = true 24 | AllowOutput bool = false 25 | RequireOutput bool = false 26 | Permissions string = `apiVersion: rbac.authorization.k8s.io/v1 27 | kind: ClusterRole 28 | metadata: 29 | name: falco-talon 30 | rules: 31 | - apiGroups: 32 | - "" 33 | resources: 34 | - pods 35 | verbs: 36 | - get 37 | - list 38 | - apiGroups: 39 | - "" 40 | resources: 41 | - pods/exec 42 | verbs: 43 | - get 44 | - create 45 | ` 46 | Example string = `- action: Run a script into the pod 47 | actionner: kubernetes:script 48 | parameters: 49 | shell: /bin/bash 50 | script: | 51 | ps awxuf 52 | netstat -lpauten 53 | top -n 1 54 | cat ${FD_NAME} 55 | ` 56 | ) 57 | 58 | var ( 59 | RequiredOutputFields = []string{"k8s.ns.name", "k8s.pod.name"} 60 | ) 61 | 62 | type Parameters struct { 63 | Script string `mapstructure:"script" validate:"omitempty"` 64 | File string `mapstructure:"file" validate:"omitempty"` 65 | Shell string `mapstructure:"shell" validate:"omitempty"` 66 | } 67 | 68 | type Actionner struct{} 69 | 70 | func Register() *Actionner { 71 | return new(Actionner) 72 | } 73 | 74 | func (a Actionner) Init() error { 75 | return k8s.Init() 76 | } 77 | 78 | func (a Actionner) Information() models.Information { 79 | return models.Information{ 80 | Name: Name, 81 | FullName: Category + ":" + Name, 82 | Category: Category, 83 | Description: Description, 84 | Source: Source, 85 | RequiredOutputFields: RequiredOutputFields, 86 | Permissions: Permissions, 87 | Example: Example, 88 | Continue: Continue, 89 | AllowOutput: AllowOutput, 90 | RequireOutput: RequireOutput, 91 | } 92 | } 93 | func (a Actionner) Parameters() models.Parameters { 94 | return Parameters{ 95 | Script: "", 96 | File: "", 97 | Shell: "/bin/sh", 98 | } 99 | } 100 | 101 | func (a Actionner) Checks(event *events.Event, _ *rules.Action) error { 102 | return k8sChecks.CheckPodExist(event) 103 | } 104 | 105 | func (a Actionner) Run(event *events.Event, action *rules.Action) (utils.LogLine, *models.Data, error) { 106 | pod := event.GetPodName() 107 | namespace := event.GetNamespaceName() 108 | 109 | objects := map[string]string{ 110 | "pod": pod, 111 | "namespace": namespace, 112 | } 113 | 114 | var parameters Parameters 115 | err := utils.DecodeParams(action.GetParameters(), ¶meters) 116 | if err != nil { 117 | return utils.LogLine{ 118 | Objects: nil, 119 | Error: err.Error(), 120 | Status: utils.FailureStr, 121 | }, nil, err 122 | } 123 | 124 | shell := new(string) 125 | if parameters.Shell != "" { 126 | *shell = parameters.Shell 127 | } else { 128 | *shell = "/bin/sh" 129 | } 130 | 131 | script := new(string) 132 | if parameters.Script != "" { 133 | *script = parameters.Script 134 | } 135 | 136 | if parameters.File != "" { 137 | fileContent, err2 := os.ReadFile(parameters.File) 138 | if err2 != nil { 139 | return utils.LogLine{ 140 | Objects: objects, 141 | Error: err2.Error(), 142 | Status: utils.FailureStr, 143 | }, 144 | nil, 145 | err2 146 | } 147 | *script = string(fileContent) 148 | } 149 | 150 | event.ExportEnvVars() 151 | *script = os.ExpandEnv(*script) 152 | 153 | client := k8s.GetClient() 154 | 155 | p, _ := client.GetPod(pod, namespace) 156 | containers := k8s.GetContainers(p) 157 | if len(containers) == 0 { 158 | err = fmt.Errorf("no container found") 159 | return utils.LogLine{ 160 | Objects: objects, 161 | Error: err.Error(), 162 | Status: utils.FailureStr, 163 | }, 164 | nil, 165 | err 166 | } 167 | 168 | // copy the script to /tmp of the pod 169 | var container string 170 | output := new(bytes.Buffer) 171 | for i, j := range containers { 172 | container = j 173 | command := []string{"tee", "/tmp/talon-script.sh", ">", "/dev/null"} 174 | _, err = client.Exec(namespace, pod, container, command, *script) 175 | if err != nil { 176 | if i == len(containers)-1 { 177 | return utils.LogLine{ 178 | Objects: objects, 179 | Error: err.Error(), 180 | Status: utils.FailureStr, 181 | }, nil, err 182 | } 183 | continue 184 | } 185 | } 186 | 187 | // run the script 188 | command := []string{*shell, "/tmp/talon-script.sh"} 189 | output, err = client.Exec(namespace, pod, container, command, "") 190 | if err != nil { 191 | return utils.LogLine{ 192 | Objects: objects, 193 | Error: err.Error(), 194 | Status: utils.FailureStr, 195 | }, nil, err 196 | } 197 | 198 | return utils.LogLine{ 199 | Objects: objects, 200 | Output: utils.RemoveAnsiCharacters(output.String()), 201 | Status: utils.SuccessStr, 202 | }, 203 | nil, 204 | nil 205 | } 206 | 207 | func (a Actionner) CheckParameters(action *rules.Action) error { 208 | var parameters Parameters 209 | 210 | err := utils.DecodeParams(action.GetParameters(), ¶meters) 211 | if err != nil { 212 | return err 213 | } 214 | 215 | err = utils.ValidateStruct(parameters) 216 | if err != nil { 217 | return err 218 | } 219 | 220 | err = validateParameters(parameters) 221 | if err != nil { 222 | return err 223 | } 224 | return nil 225 | } 226 | 227 | func validateParameters(parameters Parameters) error { 228 | if parameters.Script == "" && parameters.File == "" { 229 | return errors.New("missing parameter 'script' or 'file'") 230 | } 231 | if parameters.Script != "" && parameters.File != "" { 232 | return errors.New("'script' and 'file' parameters can't be set at the same time") 233 | } 234 | if parameters.File != "" { 235 | _, err := os.Stat(parameters.File) 236 | if os.IsNotExist(err) { 237 | return err 238 | } 239 | } 240 | return nil 241 | } 242 | -------------------------------------------------------------------------------- /actionners/kubernetes/tcpdump/tcpdump.go: -------------------------------------------------------------------------------- 1 | package tcpdump 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/google/uuid" 7 | 8 | "github.com/falcosecurity/falco-talon/internal/events" 9 | k8sChecks "github.com/falcosecurity/falco-talon/internal/kubernetes/checks" 10 | k8s "github.com/falcosecurity/falco-talon/internal/kubernetes/client" 11 | "github.com/falcosecurity/falco-talon/internal/models" 12 | "github.com/falcosecurity/falco-talon/internal/rules" 13 | "github.com/falcosecurity/falco-talon/utils" 14 | ) 15 | 16 | const ( 17 | Name string = "tcpdump" 18 | Category string = "kubernetes" 19 | Description string = "Capture the network packets in a pod" 20 | Source string = "syscalls" 21 | Continue bool = false 22 | UseContext bool = false 23 | AllowOutput bool = false 24 | RequireOutput bool = true 25 | Permissions string = `apiVersion: rbac.authorization.k8s.io/v1 26 | kind: ClusterRole 27 | metadata: 28 | name: falco-talon 29 | rules: 30 | - apiGroups: 31 | - "" 32 | resources: 33 | - pods 34 | verbs: 35 | - get 36 | - update 37 | - patch 38 | - list 39 | - apiGroups: 40 | - "" 41 | resources: 42 | - pods/ephemeralcontainers 43 | verbs: 44 | - patch 45 | - create 46 | - apiGroups: 47 | - "" 48 | resources: 49 | - pods/exec 50 | verbs: 51 | - get 52 | - create 53 | ` 54 | Example string = `- action: Create a packet capture from a pod 55 | actionner: kubernetes:tcpdump 56 | parameters: 57 | duration: 10 58 | snaplen: 1024 59 | output: 60 | target: aws:s3 61 | parameters: 62 | bucket: my-bucket 63 | prefix: /captures/ 64 | ` 65 | ) 66 | 67 | var ( 68 | RequiredOutputFields = []string{"k8s.ns.name", "k8s.pod.name"} 69 | ) 70 | 71 | type Parameters struct { 72 | Image string `mapstructure:"image"` 73 | Duration int `mapstructure:"duration" validate:"gte=0"` 74 | Snaplen int `mapstructure:"snaplen" validate:"gte=0"` 75 | } 76 | 77 | const ( 78 | baseName string = "falco-talon-tcpdump-" 79 | defaultImage string = "issif/tcpdump:latest" 80 | defaultTTL int = 60 81 | defaultDuration int = 5 82 | defaulSnaplen int = 4096 83 | ) 84 | 85 | type Actionner struct{} 86 | 87 | func Register() *Actionner { 88 | return new(Actionner) 89 | } 90 | 91 | func (a Actionner) Init() error { 92 | return k8s.Init() 93 | } 94 | 95 | func (a Actionner) Information() models.Information { 96 | return models.Information{ 97 | Name: Name, 98 | FullName: Category + ":" + Name, 99 | Category: Category, 100 | Description: Description, 101 | Source: Source, 102 | RequiredOutputFields: RequiredOutputFields, 103 | Permissions: Permissions, 104 | Example: Example, 105 | Continue: Continue, 106 | AllowOutput: AllowOutput, 107 | RequireOutput: RequireOutput, 108 | } 109 | } 110 | 111 | func (a Actionner) Parameters() models.Parameters { 112 | return Parameters{ 113 | Duration: defaultDuration, 114 | Snaplen: defaulSnaplen, 115 | Image: defaultImage, 116 | } 117 | } 118 | 119 | func (a Actionner) Checks(event *events.Event, _ *rules.Action) error { 120 | return k8sChecks.CheckPodExist(event) 121 | } 122 | 123 | func (a Actionner) Run(event *events.Event, action *rules.Action) (utils.LogLine, *models.Data, error) { 124 | podName := event.GetPodName() 125 | namespace := event.GetNamespaceName() 126 | 127 | objects := map[string]string{ 128 | "pod": podName, 129 | "namespace": namespace, 130 | } 131 | 132 | var parameters Parameters 133 | err := utils.DecodeParams(action.GetParameters(), ¶meters) 134 | if err != nil { 135 | return utils.LogLine{ 136 | Objects: nil, 137 | Error: err.Error(), 138 | Status: utils.FailureStr, 139 | }, nil, err 140 | } 141 | 142 | if parameters.Duration == 0 { 143 | parameters.Duration = defaultDuration 144 | } 145 | 146 | if parameters.Image == "" { 147 | parameters.Image = defaultImage 148 | } 149 | 150 | client := k8s.GetClient() 151 | 152 | pod, _ := client.GetPod(podName, namespace) 153 | containers := k8s.GetContainers(pod) 154 | if len(containers) == 0 { 155 | err = fmt.Errorf("no container found") 156 | return utils.LogLine{ 157 | Objects: objects, 158 | Error: err.Error(), 159 | Status: utils.FailureStr, 160 | }, nil, err 161 | } 162 | 163 | ephemeralContainerName := fmt.Sprintf("%v%v", baseName, uuid.NewString()[:5]) 164 | 165 | err = client.CreateEphemeralContainer(pod, containers[0], ephemeralContainerName, parameters.Image, defaultTTL) 166 | if err != nil { 167 | return utils.LogLine{ 168 | Objects: objects, 169 | Error: err.Error(), 170 | Status: utils.FailureStr, 171 | }, nil, err 172 | } 173 | 174 | command := []string{"tee", "/tmp/talon-script.sh", "/dev/null"} 175 | script := fmt.Sprintf("timeout %vs tcpdump -n -i any -s %v -w /tmp/tcpdump.pcap || [ $? -eq 124 ] && echo OK || exit 1\n", parameters.Duration, parameters.Snaplen) 176 | _, err = client.Exec(namespace, podName, ephemeralContainerName, command, script) 177 | if err != nil { 178 | return utils.LogLine{ 179 | Objects: objects, 180 | Error: err.Error(), 181 | Status: utils.FailureStr, 182 | }, nil, err 183 | } 184 | 185 | command = []string{"sh", "/tmp/talon-script.sh"} 186 | _, err = client.Exec(namespace, podName, ephemeralContainerName, command, "") 187 | if err != nil { 188 | return utils.LogLine{ 189 | Objects: objects, 190 | Error: err.Error(), 191 | Status: utils.FailureStr, 192 | }, nil, err 193 | } 194 | 195 | command = []string{"cat", "/tmp/tcpdump.pcap"} 196 | output, err := client.Exec(namespace, podName, ephemeralContainerName, command, "") 197 | if err != nil { 198 | return utils.LogLine{ 199 | Objects: objects, 200 | Error: err.Error(), 201 | Status: utils.FailureStr, 202 | }, nil, err 203 | } 204 | 205 | return utils.LogLine{ 206 | Objects: objects, 207 | Output: fmt.Sprintf("a tcpdump '%v' has been created", "tcpdump.pcap"), 208 | Status: utils.SuccessStr, 209 | }, &models.Data{Name: "tcpdump.pcap", Objects: objects, Bytes: output.Bytes()}, nil 210 | } 211 | 212 | func (a Actionner) CheckParameters(action *rules.Action) error { 213 | var parameters Parameters 214 | 215 | err := utils.DecodeParams(action.GetParameters(), ¶meters) 216 | if err != nil { 217 | return err 218 | } 219 | 220 | err = utils.ValidateStruct(parameters) 221 | if err != nil { 222 | return err 223 | } 224 | 225 | return nil 226 | } 227 | -------------------------------------------------------------------------------- /cmd/actionners.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/spf13/cobra" 9 | "gopkg.in/yaml.v3" 10 | 11 | "github.com/falcosecurity/falco-talon/actionners" 12 | ) 13 | 14 | var actionnersCmd = &cobra.Command{ 15 | Use: "actionners", 16 | Short: "Manage the actionners", 17 | Long: "Manage the actionners", 18 | Run: nil, 19 | } 20 | 21 | var actionnersListCmd = &cobra.Command{ 22 | Use: "list", 23 | Short: "List the available Actionners", 24 | Long: "List the available Actionners.", 25 | Run: func(_ *cobra.Command, _ []string) { 26 | defaultActionners := actionners.ListDefaultActionners() 27 | type actionner struct { // nolint:govet 28 | Parameters map[string]any `yaml:"parameters"` 29 | Name string `yaml:"name"` 30 | Category string `yaml:"category"` 31 | Description string `yaml:"description"` 32 | Source string `yaml:"source"` 33 | Permissions string `yaml:"permissions,omitempty"` 34 | Example string `yaml:"example,omitempty"` 35 | RequiredOutputFields []string `yaml:"required_output_fields"` 36 | Continue bool `yaml:"continue"` 37 | UseContext bool `yaml:"use_context"` 38 | AllowOutput bool `yaml:"allow_output"` 39 | RequireOutput bool `yaml:"require_output"` 40 | } 41 | 42 | for _, i := range *defaultActionners { 43 | parameters := make(map[string]any) 44 | a := actionner{ 45 | Name: i.Information().Name, 46 | Category: i.Information().Category, 47 | Description: i.Information().Description, 48 | Source: i.Information().Source, 49 | RequiredOutputFields: i.Information().RequiredOutputFields, 50 | Continue: i.Information().Continue, 51 | UseContext: i.Information().UseContext, 52 | AllowOutput: i.Information().AllowOutput, 53 | RequireOutput: i.Information().RequireOutput, 54 | Permissions: i.Information().Permissions, 55 | Example: i.Information().Example, 56 | } 57 | 58 | if p := i.Parameters(); p != nil { 59 | valueOf := reflect.ValueOf(i.Parameters()) 60 | if valueOf.Kind() == reflect.Ptr { 61 | valueOf = valueOf.Elem() 62 | } 63 | 64 | for i := 0; i < valueOf.NumField(); i++ { 65 | field := valueOf.Type().Field(i) 66 | var r string 67 | if strings.Contains(field.Tag.Get("validate"), "required") { 68 | r = requiredStr 69 | } 70 | parameters[field.Tag.Get("mapstructure")+r] = valueOf.Field(i).Interface() 71 | } 72 | 73 | a.Parameters = parameters 74 | } 75 | yamla, _ := yaml.Marshal(a) 76 | fmt.Printf("--- %v ---\n\n", i.Information().FullName) 77 | fmt.Println(string(yamla)) 78 | } 79 | }, 80 | } 81 | -------------------------------------------------------------------------------- /cmd/notifiers.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/spf13/cobra" 9 | "gopkg.in/yaml.v3" 10 | 11 | "github.com/falcosecurity/falco-talon/notifiers" 12 | ) 13 | 14 | var notifiersCmd = &cobra.Command{ 15 | Use: "notifiers", 16 | Short: "Manage the Notifiers", 17 | Long: "Manage the Notifiers.", 18 | Run: nil, 19 | } 20 | 21 | var notifiersListCmd = &cobra.Command{ 22 | Use: "list", 23 | Short: "List the available Notifiers", 24 | Long: "List the available Notifiers.", 25 | Run: func(_ *cobra.Command, _ []string) { 26 | defaultOuputs := notifiers.ListDefaultNotifiers() 27 | type output struct { //nolint:govet 28 | Name string `yaml:"name"` 29 | Description string `yaml:"description"` 30 | Parameters map[string]any `yaml:"parameters,omitempty"` 31 | Permissions string `yaml:"permissions,omitempty"` 32 | Example string `yaml:"example,omitempty"` 33 | } 34 | 35 | for _, i := range *defaultOuputs { 36 | parameters := make(map[string]any) 37 | a := output{ 38 | Name: i.Information().Name, 39 | Description: i.Information().Description, 40 | Permissions: i.Information().Permissions, 41 | Example: i.Information().Example, 42 | } 43 | 44 | if p := i.Parameters(); p != nil { 45 | valueOf := reflect.ValueOf(i.Parameters()) 46 | if valueOf.Kind() == reflect.Ptr { 47 | valueOf = valueOf.Elem() 48 | } 49 | 50 | for i := 0; i < valueOf.NumField(); i++ { 51 | field := valueOf.Type().Field(i) 52 | var r string 53 | if strings.Contains(field.Tag.Get("validate"), "required") { 54 | r = requiredStr 55 | } 56 | parameters[field.Tag.Get("field")+r] = valueOf.Field(i).Interface() 57 | } 58 | 59 | a.Parameters = parameters 60 | } 61 | yamla, _ := yaml.Marshal(a) 62 | fmt.Printf("--- %v ---\n\n", i.Information().FullName) 63 | fmt.Println(string(yamla)) 64 | } 65 | }, 66 | } 67 | -------------------------------------------------------------------------------- /cmd/outputs.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/spf13/cobra" 9 | "gopkg.in/yaml.v3" 10 | 11 | "github.com/falcosecurity/falco-talon/outputs" 12 | ) 13 | 14 | var outputsCmd = &cobra.Command{ 15 | Use: "outputs", 16 | Short: "Manage the Outputs", 17 | Long: "Manage the Outputs.", 18 | Run: nil, 19 | } 20 | 21 | var outputsListCmd = &cobra.Command{ 22 | Use: "list", 23 | Short: "List the available Outputs", 24 | Long: "List the available Outputs.", 25 | Run: func(_ *cobra.Command, _ []string) { 26 | defaultOuputs := outputs.ListDefaultOutputs() 27 | type output struct { //nolint:govet 28 | Name string `yaml:"name"` 29 | Category string `yaml:"category"` 30 | Description string `yaml:"description"` 31 | Parameters map[string]any `yaml:"parameters"` 32 | Permissions string `yaml:"permissions,omitempty"` 33 | Example string `yaml:"example,omitempty"` 34 | } 35 | 36 | for _, i := range *defaultOuputs { 37 | parameters := make(map[string]any) 38 | a := output{ 39 | Name: i.Information().Name, 40 | Category: i.Information().Category, 41 | Description: i.Information().Description, 42 | Permissions: i.Information().Permissions, 43 | Example: i.Information().Example, 44 | } 45 | 46 | if p := i.Parameters(); p != nil { 47 | valueOf := reflect.ValueOf(i.Parameters()) 48 | if valueOf.Kind() == reflect.Ptr { 49 | valueOf = valueOf.Elem() 50 | } 51 | 52 | for i := 0; i < valueOf.NumField(); i++ { 53 | field := valueOf.Type().Field(i) 54 | var r string 55 | if strings.Contains(field.Tag.Get("validate"), "required") { 56 | r = requiredStr 57 | } 58 | parameters[field.Tag.Get("mapstructure")+r] = valueOf.Field(i).Interface() 59 | } 60 | 61 | a.Parameters = parameters 62 | } 63 | yamla, _ := yaml.Marshal(a) 64 | fmt.Printf("--- %v ---\n\n", i.Information().FullName) 65 | fmt.Println(string(yamla)) 66 | } 67 | }, 68 | } 69 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/falcosecurity/falco-talon/utils" 7 | ) 8 | 9 | const ( 10 | requiredStr = " (required)" 11 | ) 12 | 13 | var RootCmd = &cobra.Command{ 14 | Use: "falco-talon", 15 | Short: "Falco Talon is a Response Engine for managing threats in Kubernetes", 16 | Long: `Falco Talon is a Response Engine for managing threats in Kubernetes 17 | It enhances the solutions proposed by Falco community with a dedicated, 18 | no-code solution. With easy rules, you can perform actions over compromised pods.`, 19 | } 20 | 21 | func Execute() { 22 | err := RootCmd.Execute() 23 | if err != nil { 24 | utils.PrintLog(utils.FatalStr, utils.LogLine{Error: err.Error()}) 25 | } 26 | } 27 | 28 | func init() { 29 | RootCmd.AddCommand(serverCmd) 30 | RootCmd.AddCommand(rulesCmd) 31 | RootCmd.AddCommand(actionnersCmd) 32 | RootCmd.AddCommand(outputsCmd) 33 | RootCmd.AddCommand(notifiersCmd) 34 | rulesCmd.AddCommand(rulesChecksCmd) 35 | rulesCmd.AddCommand(rulesPrintCmd) 36 | actionnersCmd.AddCommand(actionnersListCmd) 37 | outputsCmd.AddCommand(outputsListCmd) 38 | notifiersCmd.AddCommand(notifiersListCmd) 39 | RootCmd.PersistentFlags().StringArrayP("rules", "r", []string{}, "Falco Talon Rules File") 40 | serverCmd.Flags().StringP("config", "c", "/etc/falco-talon/config.yaml", "Falco Talon Config File") 41 | rulesCmd.PersistentFlags().StringP("config", "c", "", "Falco Talon Config File") 42 | actionnersCmd.PersistentFlags().StringP("config", "c", "", "Falco Talon Config File") 43 | outputsCmd.PersistentFlags().StringP("config", "c", "", "Falco Talon Config File") 44 | notifiersCmd.PersistentFlags().StringP("config", "c", "", "Falco Talon Config File") 45 | } 46 | -------------------------------------------------------------------------------- /cmd/rules.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/falcosecurity/falco-talon/actionners" 7 | "github.com/falcosecurity/falco-talon/configuration" 8 | ruleengine "github.com/falcosecurity/falco-talon/internal/rules" 9 | "github.com/falcosecurity/falco-talon/outputs" 10 | "github.com/falcosecurity/falco-talon/utils" 11 | 12 | "github.com/jinzhu/copier" 13 | "github.com/spf13/cobra" 14 | "gopkg.in/yaml.v2" 15 | ) 16 | 17 | var rulesCmd = &cobra.Command{ 18 | Use: "rules", 19 | Short: "Manage Falco Talon rules", 20 | Long: `Manage the rules loaded by Falco Talon. You can print them in the stdout or check their validity.`, 21 | } 22 | 23 | var rulesChecksCmd = &cobra.Command{ 24 | Use: "check", 25 | Short: "Check Falco Talon Rules file", 26 | Long: "Check Falco Talon Rules file", 27 | Run: func(cmd *cobra.Command, _ []string) { 28 | configFile, _ := cmd.Flags().GetString("config") 29 | config := configuration.CreateConfiguration(configFile) 30 | utils.SetLogFormat(config.LogFormat) 31 | rulesFiles, _ := cmd.Flags().GetStringArray("rules") 32 | if len(rulesFiles) != 0 { 33 | config.RulesFiles = rulesFiles 34 | } 35 | rules := ruleengine.ParseRules(config.RulesFiles) 36 | if rules == nil { 37 | utils.PrintLog(utils.FatalStr, utils.LogLine{Error: "invalid rules", Message: "rules"}) 38 | } 39 | defaultActionners := actionners.ListDefaultActionners() 40 | defaultOutputs := outputs.ListDefaultOutputs() 41 | 42 | valid := true 43 | if rules != nil { 44 | for _, i := range *rules { 45 | for _, j := range i.GetActions() { 46 | actionner := defaultActionners.FindActionner(j.GetActionner()) 47 | if actionner == nil { 48 | utils.PrintLog(utils.ErrorStr, utils.LogLine{Error: "unknown actionner", Rule: i.GetName(), Action: j.GetName(), Actionner: j.GetActionner(), Message: "rules"}) 49 | valid = false 50 | continue 51 | } 52 | if err := actionner.CheckParameters(j); err != nil { 53 | utils.PrintLog(utils.ErrorStr, utils.LogLine{Error: err.Error(), Rule: i.GetName(), Action: j.GetName(), Actionner: j.GetActionner(), Message: "rules"}) 54 | valid = false 55 | } 56 | o := j.GetOutput() 57 | if o == nil && actionner.Information().RequireOutput { 58 | utils.PrintLog(utils.ErrorStr, utils.LogLine{Error: "an output is required", Rule: i.GetName(), Action: j.GetName(), Actionner: j.GetActionner(), Message: "rules"}) 59 | valid = false 60 | } 61 | if actionner != nil { 62 | o := j.GetOutput() 63 | if o == nil && actionner.Information().RequireOutput { 64 | utils.PrintLog(utils.ErrorStr, utils.LogLine{Error: "an output is required", Rule: i.GetName(), Action: j.GetName(), Actionner: j.GetActionner(), Message: "rules"}) 65 | valid = false 66 | } 67 | if o != nil { 68 | output := defaultOutputs.FindOutput(o.GetTarget()) 69 | if output == nil { 70 | utils.PrintLog(utils.ErrorStr, utils.LogLine{Error: "unknown target", Rule: i.GetName(), Action: j.GetName(), OutputTarget: o.GetTarget(), Message: "rules"}) 71 | valid = false 72 | } else if len(o.Parameters) == 0 { 73 | utils.PrintLog(utils.ErrorStr, utils.LogLine{Error: "missing parameters for the output", Rule: i.GetName(), Action: j.GetName(), OutputTarget: o.GetTarget(), Message: "rules"}) 74 | valid = false 75 | } else { 76 | if err := output.CheckParameters(o); err != nil { 77 | utils.PrintLog(utils.ErrorStr, utils.LogLine{Error: err.Error(), Rule: i.GetName(), Action: j.GetName(), OutputTarget: o.GetTarget(), Message: "rules"}) 78 | valid = false 79 | } 80 | } 81 | } 82 | } 83 | } 84 | } 85 | } 86 | if !valid { 87 | utils.PrintLog(utils.FatalStr, utils.LogLine{Error: "invalid rules", Message: "rules"}) 88 | } 89 | utils.PrintLog(utils.InfoStr, utils.LogLine{Result: "rules file valid", Message: "rules"}) 90 | }, 91 | } 92 | 93 | var rulesPrintCmd = &cobra.Command{ 94 | Use: "print", 95 | Short: "Print the loaded by Falco Talon in the stdout", 96 | Long: "Print the loaded by Falco Talon in the stdout.", 97 | Run: func(cmd *cobra.Command, _ []string) { 98 | configFile, _ := cmd.Flags().GetString("config") 99 | config := configuration.CreateConfiguration(configFile) 100 | utils.SetLogFormat(config.LogFormat) 101 | rulesFiles, _ := cmd.Flags().GetStringArray("rules") 102 | if len(rulesFiles) != 0 { 103 | config.RulesFiles = rulesFiles 104 | } 105 | rules := ruleengine.ParseRules(config.RulesFiles) 106 | if rules == nil { 107 | utils.PrintLog(utils.FatalStr, utils.LogLine{Error: "invalid rules", Message: "rules"}) 108 | } 109 | type yamlFile struct { 110 | Name string `yaml:"rule"` 111 | Description string `yaml:"description,omitempty"` 112 | Continue string `yaml:"continue,omitempty"` 113 | DryRun string `yaml:"dry_run,omitempty"` 114 | Notifiers []string `yaml:"notifiers,omitempty"` 115 | Actions []struct { 116 | Parameters map[string]any `yaml:"parameters,omitempty"` 117 | Output struct { 118 | Parameters map[string]any `yaml:"parameters"` 119 | Target string `yaml:"target"` 120 | } `yaml:"output,omitempty"` 121 | Name string `yaml:"action"` 122 | Description string `yaml:"description,omitempty"` 123 | Actionner string `yaml:"actionner"` 124 | Continue string `yaml:"continue,omitempty"` 125 | IgnoreErrors string `yaml:"ignore_errors,omitempty"` 126 | AdditionalContexts []string `yaml:"additional_contexts,omitempty"` 127 | } `yaml:"actions"` 128 | Match struct { 129 | OutputFields []string `yaml:"output_fields,omitempty"` 130 | Priority string `yaml:"priority,omitempty"` 131 | Source string `yaml:"source,omitempty"` 132 | Rules []string `yaml:"rules,omitempty"` 133 | Tags []string `yaml:"tags,omitempty"` 134 | } `yaml:"match"` 135 | } 136 | 137 | var q []yamlFile 138 | if err := copier.Copy(&q, &rules); err != nil { 139 | utils.PrintLog(utils.FatalStr, utils.LogLine{Error: err.Error()}) 140 | } 141 | 142 | b, _ := yaml.Marshal(q) 143 | fmt.Printf("---\n%s", b) 144 | }, 145 | } 146 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/falcosecurity/falco-talon/configuration" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var versionCmd = &cobra.Command{ 12 | Use: "version", 13 | Short: "Print version of Falco Talon.", 14 | Long: "Print version of Falco Talon", 15 | Run: func(_ *cobra.Command, _ []string) { 16 | fmt.Println(configuration.GetInfo().String()) 17 | }, 18 | } 19 | 20 | func init() { 21 | RootCmd.AddCommand(versionCmd) 22 | } 23 | -------------------------------------------------------------------------------- /config_example.yaml: -------------------------------------------------------------------------------- 1 | listen_address: "0.0.0.0" # default: "0.0.0.0" 2 | listen_port: "2803" # default: "2803" 3 | rules_files: 4 | - "./rules.yaml" # default: "./rules.yaml" 5 | # kubeConfig: "~/.kube/config" # only if Falco Talon is running outside Kubernetes 6 | log_format: "color" # log Format: text, color, json (default: color) 7 | watch_rules: true # reload if the rules files changes (default: true) 8 | print_all_events: true # print in logs all received events, not only those which match 9 | otel: 10 | traces_enabled: true 11 | metrics_enabled: true 12 | collector_port: 4317 13 | collector_endpoint: localhost 14 | collector_use_insecure_grpc: true 15 | timeout: 10 16 | 17 | deduplication: 18 | leader_election: true # enable the leader election for cluster mode (in k8s only) 19 | time_window_seconds: 5 # duration in seconds for the deduplication time window (default: 5) 20 | 21 | default_notifiers: # these notifiers will be enabled for all rules 22 | - k8sevents 23 | 24 | # aws: 25 | # role_arn: arn:aws:iam:::role/ 26 | # external_id: 27 | # region: # if not specified, default region from provider credential chain will be used 28 | # access_key: # if not specified, default access_key from provider credential chain will be used 29 | # secret_key: # if not specified, default secret_key from provider credential chain will be used 30 | 31 | # minio: 32 | # endpoint: # endpoint 33 | # access_key: # access key 34 | # secret_key: # secret key 35 | # use_ssl: false # Use SSL 36 | 37 | notifiers: 38 | slack: 39 | webhook_url: "https://hooks.slack.com/services/XXXX" 40 | # icon: "" # default: "https://upload.wikimedia.org/wikipedia/commons/2/26/Circaetus_gallicus_claw.jpg" 41 | # username: "" # default: "Falco Talon" 42 | footer: "" # default: "https://github.com/falcosecurity/falco-talon" 43 | format: long # default: long 44 | # webhook: 45 | # url: "" 46 | # smtp: 47 | # host_port: "" 48 | # from: "" 49 | # to: "" 50 | # user: "" 51 | # password: "" 52 | # format: "html" -------------------------------------------------------------------------------- /configuration/configuration.go: -------------------------------------------------------------------------------- 1 | package configuration 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/spf13/viper" 8 | 9 | "github.com/falcosecurity/falco-talon/utils" 10 | ) 11 | 12 | const ( 13 | defaultListenAddress string = "0.0.0.0" 14 | defaultListPort int = 2803 15 | defaultRulesFile string = "/etc/falco-talon/rules.yaml" 16 | defaultWatchRules bool = true 17 | defaultPrintAllEvents bool = false 18 | defaultDeduplicationLeaderElection bool = true 19 | defaultDeduplicationTimeWindow int = 5 20 | defaultOtelCollectorTracesEnabled bool = false 21 | defaultOtelCollectorMetricsEnabled bool = false 22 | defaultOtelCollectorEndpoint string = "localhost" 23 | defaultOtelCollectorUseInsecureGrpc bool = false 24 | defaultOtelCollectorPort int = 4317 25 | defaultOtelCollectorGRPCTimeout = 10 26 | ) 27 | 28 | type Otel struct { 29 | CollectorEndpoint string `mapstructure:"collector_endpoint"` 30 | CollectorPort string `mapstructure:"collector_port"` 31 | Timeout int `mapstructure:"timeout"` 32 | CollectorUseInsecureGrpc bool `mapstructure:"collector_use_insecure_grpc"` 33 | TracesEnabled bool `mapstructure:"traces_enabled"` 34 | MetricsEnabled bool `mapstructure:"metrics_enabled"` 35 | } 36 | 37 | type Configuration struct { 38 | Notifiers map[string]map[string]interface{} `mapstructure:"notifiers"` 39 | AwsConfig AwsConfig `mapstructure:"aws"` 40 | GcpConfig GcpConfig `mapstructure:"gcp"` 41 | LogFormat string `mapstructure:"log_format"` 42 | KubeConfig string `mapstructure:"kubeconfig"` 43 | ListenAddress string `mapstructure:"listen_address"` 44 | MinioConfig MinioConfig `mapstructure:"minio"` 45 | RulesFiles []string `mapstructure:"rules_files"` 46 | DefaultNotifiers []string `mapstructure:"default_notifiers"` 47 | Otel Otel `mapstructure:"otel"` 48 | Deduplication deduplication `mapstructure:"deduplication"` 49 | ListenPort int `mapstructure:"listen_port"` 50 | WatchRules bool `mapstructure:"watch_rules"` 51 | PrintAllEvents bool `mapstructure:"print_all_events"` 52 | } 53 | 54 | type deduplication struct { 55 | LeaderElection bool `mapstructure:"leader_election"` 56 | TimeWindowSeconds int `mapstructure:"time_window_seconds"` 57 | } 58 | 59 | type AwsConfig struct { 60 | Region string `mapstructure:"region"` 61 | AccessKey string `mapstructure:"access_key"` 62 | SecretKey string `mapstructure:"secret_key"` 63 | RoleArn string `mapstructure:"role_arn"` 64 | ExternalID string `mapstructure:"external_id"` 65 | } 66 | 67 | type GcpConfig struct { 68 | Region string `mapstructure:"region"` 69 | CredentialsPath string `mapstructure:"credentials_path"` 70 | } 71 | 72 | type MinioConfig struct { 73 | Endpoint string `mapstructure:"endpoint"` 74 | AccessKey string `mapstructure:"access_key"` 75 | SecretKey string `mapstructure:"secret_key"` 76 | UseSSL bool `mapstructure:"use_ssl"` 77 | } 78 | 79 | var config *Configuration 80 | 81 | func init() { 82 | config = new(Configuration) 83 | } 84 | 85 | func CreateConfiguration(configFile string) *Configuration { 86 | v := viper.New() 87 | v.SetDefault("listen_address", defaultListenAddress) 88 | v.SetDefault("listen_port", defaultListPort) 89 | v.SetDefault("rules_files", []string{defaultRulesFile}) 90 | v.SetDefault("kubeconfig", "") 91 | v.SetDefault("log_format", "color") 92 | v.SetDefault("default_notifiers", []string{}) 93 | v.SetDefault("watch_rules", defaultWatchRules) 94 | v.SetDefault("print_all_events", defaultPrintAllEvents) 95 | v.SetDefault("deduplication.leader_election", defaultDeduplicationLeaderElection) 96 | v.SetDefault("deduplication.time_window_seconds", defaultDeduplicationTimeWindow) 97 | v.SetDefault("otel.traces_enabled", defaultOtelCollectorTracesEnabled) 98 | v.SetDefault("otel.metrics_enabled", defaultOtelCollectorMetricsEnabled) 99 | v.SetDefault("otel.collector_endpoint", defaultOtelCollectorEndpoint) 100 | v.SetDefault("otel.collector_port", defaultOtelCollectorPort) 101 | v.Set("otel.timeout", defaultOtelCollectorGRPCTimeout) 102 | v.SetDefault("otel.collector_use_insecure_grpc", defaultOtelCollectorUseInsecureGrpc) 103 | v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) 104 | v.AutomaticEnv() 105 | 106 | if configFile != "" { 107 | v.SetConfigFile(configFile) 108 | err := v.ReadInConfig() 109 | if err != nil { 110 | utils.PrintLog(utils.FatalStr, utils.LogLine{Error: fmt.Sprintf("error when reading config file: '%v'", err.Error()), Message: "config"}) 111 | } 112 | } 113 | 114 | if err := v.Unmarshal(config); err != nil { 115 | utils.PrintLog(utils.FatalStr, utils.LogLine{Error: fmt.Sprintf("error unmarshalling config file: '%v'", err.Error()), Message: "config"}) 116 | } 117 | 118 | return config 119 | } 120 | 121 | func GetConfiguration() *Configuration { 122 | return config 123 | } 124 | 125 | func (c *Configuration) ListDefaultNotifiers() []string { 126 | return c.DefaultNotifiers 127 | } 128 | -------------------------------------------------------------------------------- /configuration/version.go: -------------------------------------------------------------------------------- 1 | package configuration 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "strings" 7 | "text/tabwriter" 8 | ) 9 | 10 | // Base version information. 11 | // 12 | // This is the fallback data used when version information from git is not 13 | // provided via go ldflags (e.g. via Makefile). 14 | var ( 15 | // Output of "git describe". The prerequisite is that the branch should be 16 | // tagged using the correct versioning strategy. 17 | GitVersion = "devel" 18 | // SHA1 from git, output of $(git rev-parse HEAD) 19 | GitCommit = "unknown" 20 | // State of git tree, either "clean" or "dirty" 21 | GitTreeState = "unknown" 22 | // Build date in ISO8601 format, output of $(date -u +'%Y-%m-%dT%H:%M:%SZ') 23 | BuildDate = "unknown" 24 | ) 25 | 26 | type Info struct { 27 | GitVersion string 28 | GitCommit string 29 | GitTreeState string 30 | BuildDate string 31 | GoVersion string 32 | Compiler string 33 | Platform string 34 | } 35 | 36 | func GetInfo() *Info { 37 | return &Info{ 38 | GitVersion: GitVersion, 39 | GitCommit: GitCommit, 40 | GitTreeState: GitTreeState, 41 | BuildDate: BuildDate, 42 | GoVersion: runtime.Version(), 43 | Compiler: runtime.Compiler, 44 | Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), 45 | } 46 | } 47 | 48 | // String returns the string representation of the version info 49 | func (i *Info) String() string { 50 | b := strings.Builder{} 51 | w := tabwriter.NewWriter(&b, 0, 0, 2, ' ', 0) 52 | 53 | fmt.Fprintf(w, "GitVersion:\t%s\n", i.GitVersion) 54 | fmt.Fprintf(w, "GitCommit:\t%s\n", i.GitCommit) 55 | fmt.Fprintf(w, "GitTreeState:\t%s\n", i.GitTreeState) 56 | fmt.Fprintf(w, "BuildDate:\t%s\n", i.BuildDate) 57 | fmt.Fprintf(w, "GoVersion:\t%s\n", i.GoVersion) 58 | fmt.Fprintf(w, "Compiler:\t%s\n", i.Compiler) 59 | fmt.Fprintf(w, "Platform:\t%s\n", i.Platform) 60 | 61 | w.Flush() 62 | return b.String() 63 | } 64 | -------------------------------------------------------------------------------- /deployment/compose/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | volumes: 2 | postgres_data: 3 | services: 4 | # tempo 5 | init: 6 | image: grafana/tempo 7 | user: root 8 | entrypoint: 9 | - "chown" 10 | - "10001:10001" 11 | - "/var/tempo" 12 | volumes: 13 | - ./tempo/data:/var/tempo 14 | tempo: 15 | image: grafana/tempo 16 | command: [ "--config.file=/etc/tempo.yaml" ] 17 | volumes: 18 | - ./tempo/tempo.yaml:/etc/tempo.yaml:ro 19 | - ./tempo/data:/var/tempo 20 | ports: 21 | - "3200" # tempo 22 | - "4317" 23 | depends_on: 24 | - init 25 | # open telemetry collector 26 | collector: 27 | image: otel/opentelemetry-collector-contrib 28 | command: ["--config=/etc/otel-collector.yaml"] 29 | volumes: 30 | - ./otel/otel-collector.yaml:/etc/otel-collector.yaml:ro 31 | ports: 32 | - "9464:9464" 33 | - "4317:4317" 34 | - "4318:4318" 35 | # minio 36 | minio: 37 | image: quay.io/minio/minio 38 | command: server /data --console-address ":9001" 39 | volumes: 40 | - ./minio/data:/data 41 | environment: 42 | - MINIO_ROOT_USER=root 43 | - MINIO_ROOT_PASSWORD=changeme123 44 | ports: 45 | - "9000:9000" 46 | - "9001:9001" 47 | # prometheus 48 | prometheus: 49 | container_name: prometheus 50 | image: prom/prometheus:latest 51 | volumes: 52 | - ./prometheus/prometheus.yaml:/etc/prometheus/prometheus.yml:ro 53 | ports: 54 | - "9090:9090" 55 | extra_hosts: 56 | - "host.docker.internal:host-gateway" 57 | # grafana 58 | grafana: 59 | image: grafana/grafana-oss 60 | container_name: grafana 61 | environment: 62 | - GF_AUTH_ANONYMOUS_ENABLED=true 63 | - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin 64 | - GF_AUTH_DISABLE_LOGIN_FORM=true 65 | volumes: 66 | - ./grafana/provisioning/:/etc/grafana/provisioning/ 67 | ports: 68 | - "3000:3000" 69 | # loki 70 | loki: 71 | image: grafana/loki:3.0.0 72 | container_name: loki 73 | ports: 74 | - "3100:3100" 75 | command: -config.file=/etc/loki/loki.yaml 76 | volumes: 77 | - ./loki:/etc/loki 78 | # jaeger 79 | jaeger: 80 | image: jaegertracing/all-in-one 81 | ports: 82 | - "16686:16686" 83 | - "14250:14250" -------------------------------------------------------------------------------- /deployment/compose/grafana/provisioning/dashboards/sample-node.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": { 7 | "type": "grafana", 8 | "uid": "-- Grafana --" 9 | }, 10 | "enable": true, 11 | "hide": true, 12 | "iconColor": "rgba(0, 211, 255, 1)", 13 | "name": "Annotations & Alerts", 14 | "target": { 15 | "limit": 100, 16 | "matchAny": false, 17 | "tags": [], 18 | "type": "dashboard" 19 | }, 20 | "type": "dashboard" 21 | } 22 | ] 23 | }, 24 | "editable": true, 25 | "fiscalYearStartMonth": 0, 26 | "graphTooltip": 0, 27 | "id": 1, 28 | "iteration": 1683510228417, 29 | "links": [], 30 | "liveNow": false, 31 | "panels": [ 32 | { 33 | "datasource": { 34 | "type": "prometheus", 35 | "uid": "PCA1D714A7EC561A2" 36 | }, 37 | "fieldConfig": { 38 | "defaults": { 39 | "color": { 40 | "mode": "palette-classic" 41 | }, 42 | "custom": { 43 | "hideFrom": { 44 | "legend": false, 45 | "tooltip": false, 46 | "viz": false 47 | } 48 | }, 49 | "mappings": [] 50 | }, 51 | "overrides": [] 52 | }, 53 | "gridPos": { 54 | "h": 8, 55 | "w": 12, 56 | "x": 0, 57 | "y": 0 58 | }, 59 | "id": 4, 60 | "options": { 61 | "legend": { 62 | "displayMode": "list", 63 | "placement": "bottom" 64 | }, 65 | "pieType": "pie", 66 | "reduceOptions": { 67 | "calcs": [ 68 | "lastNotNull" 69 | ], 70 | "fields": "", 71 | "values": false 72 | }, 73 | "tooltip": { 74 | "mode": "single", 75 | "sort": "none" 76 | } 77 | }, 78 | "pluginVersion": "9.0.1", 79 | "targets": [ 80 | { 81 | "datasource": { 82 | "type": "prometheus", 83 | "uid": "PCA1D714A7EC561A2" 84 | }, 85 | "expr": "sum(requests{path=\"/rolldice\"}) by (status)", 86 | "refId": "A" 87 | } 88 | ], 89 | "title": "Request Status Distribution", 90 | "type": "piechart" 91 | }, 92 | { 93 | "datasource": { 94 | "type": "prometheus", 95 | "uid": "PCA1D714A7EC561A2" 96 | }, 97 | "fieldConfig": { 98 | "defaults": { 99 | "color": { 100 | "mode": "thresholds" 101 | }, 102 | "mappings": [], 103 | "thresholds": { 104 | "mode": "absolute", 105 | "steps": [ 106 | { 107 | "color": "green" 108 | }, 109 | { 110 | "color": "red", 111 | "value": 80 112 | } 113 | ] 114 | } 115 | }, 116 | "overrides": [] 117 | }, 118 | "gridPos": { 119 | "h": 9, 120 | "w": 12, 121 | "x": 0, 122 | "y": 8 123 | }, 124 | "id": 2, 125 | "options": { 126 | "colorMode": "value", 127 | "graphMode": "area", 128 | "justifyMode": "auto", 129 | "orientation": "auto", 130 | "reduceOptions": { 131 | "calcs": [ 132 | "lastNotNull" 133 | ], 134 | "fields": "", 135 | "values": false 136 | }, 137 | "textMode": "auto" 138 | }, 139 | "pluginVersion": "9.0.1", 140 | "targets": [ 141 | { 142 | "datasource": { 143 | "type": "prometheus", 144 | "uid": "PCA1D714A7EC561A2" 145 | }, 146 | "editorMode": "code", 147 | "expr": "sum(request_duration_bucket{le=\"$percentage\", method=\"GET\", path=\"/rolldice\", status=\"200\"}) \n/\nsum(requests{job=\"collector\", method=\"GET\", path=\"/rolldice\", status=\"200\"})", 148 | "legendFormat": "__auto", 149 | "range": true, 150 | "refId": "A" 151 | } 152 | ], 153 | "title": "Performance percentile", 154 | "type": "stat" 155 | } 156 | ], 157 | "refresh": "", 158 | "schemaVersion": 36, 159 | "style": "dark", 160 | "tags": [], 161 | "templating": { 162 | "list": [ 163 | { 164 | "current": { 165 | "selected": false, 166 | "text": "+Inf", 167 | "value": "+Inf" 168 | }, 169 | "datasource": { 170 | "type": "prometheus", 171 | "uid": "PCA1D714A7EC561A2" 172 | }, 173 | "definition": "label_values(request_duration_bucket, le)", 174 | "hide": 0, 175 | "includeAll": false, 176 | "multi": false, 177 | "name": "percentage", 178 | "options": [], 179 | "query": { 180 | "query": "label_values(request_duration_bucket, le)", 181 | "refId": "StandardVariableQuery" 182 | }, 183 | "refresh": 1, 184 | "regex": "/^(?!0$)(?!\\+Inf$).*/", 185 | "skipUrlSync": false, 186 | "sort": 0, 187 | "type": "query" 188 | } 189 | ] 190 | }, 191 | "time": { 192 | "from": "now-6h", 193 | "to": "now" 194 | }, 195 | "timepicker": {}, 196 | "timezone": "", 197 | "title": "Default", 198 | "uid": "ZlAo7es4k", 199 | "version": 5, 200 | "weekStart": "" 201 | } -------------------------------------------------------------------------------- /deployment/compose/grafana/provisioning/datasources/default.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | datasources: 4 | - name: Prometheus 5 | type: prometheus 6 | uid: prometheus 7 | access: proxy 8 | orgId: 1 9 | url: http://prometheus:9090 10 | basicAuth: false 11 | isDefault: false 12 | version: 1 13 | editable: false 14 | jsonData: 15 | httpMethod: GET 16 | - name: Tempo 17 | type: tempo 18 | access: proxy 19 | orgId: 1 20 | url: http://tempo:3200 21 | basicAuth: false 22 | isDefault: true 23 | version: 1 24 | editable: false 25 | apiVersion: 1 26 | uid: tempo 27 | jsonData: 28 | httpMethod: GET 29 | - name: Loki 30 | type: loki 31 | access: proxy 32 | orgId: 1 33 | url: http://loki:3100 34 | basicAuth: false 35 | isDefault: false 36 | version: 1 37 | editable: false 38 | apiVersion: 1 39 | uid: loki 40 | jsonData: 41 | httpMethod: GET -------------------------------------------------------------------------------- /deployment/compose/loki/loki.yaml: -------------------------------------------------------------------------------- 1 | auth_enabled: false 2 | 3 | server: 4 | http_listen_port: 3100 5 | 6 | common: 7 | instance_addr: 0.0.0.0 8 | path_prefix: /etc/loki/data 9 | storage: 10 | filesystem: 11 | chunks_directory: /etc/loki/data/chunks 12 | rules_directory: /etc/loki/data/rules 13 | replication_factor: 1 14 | ring: 15 | kvstore: 16 | store: inmemory 17 | 18 | schema_config: 19 | configs: 20 | - from: 2020-10-24 21 | store: tsdb 22 | object_store: filesystem 23 | schema: v13 24 | index: 25 | prefix: index_ 26 | period: 24h 27 | 28 | ruler: 29 | alertmanager_url: http://localhost:9093 30 | 31 | analytics: 32 | reporting_enabled: false -------------------------------------------------------------------------------- /deployment/compose/otel/otel-collector.yaml: -------------------------------------------------------------------------------- 1 | receivers: 2 | otlp: 3 | protocols: 4 | grpc: 5 | endpoint: 0.0.0.0:4317 6 | http: 7 | endpoint: 0.0.0.0:4318 8 | 9 | exporters: 10 | otlp/tempo: 11 | endpoint: tempo:4317 12 | tls: 13 | insecure: true 14 | prometheus: 15 | endpoint: 0.0.0.0:9464 16 | otlp/jaeger: 17 | endpoint: "http://jaeger:4317" 18 | tls: 19 | insecure: true 20 | processors: 21 | batch: 22 | tail_sampling: 23 | decision_wait: 1s 24 | num_traces: 200 25 | policies: 26 | [ 27 | { 28 | name: ignore-unmatched, 29 | type: ottl_condition, 30 | ottl_condition: { 31 | error_mode: ignore, 32 | span: [ 33 | "IsMatch(name, \"match\")" 34 | ] 35 | } 36 | } 37 | ] 38 | service: 39 | telemetry: 40 | logs: 41 | level: "debug" 42 | pipelines: 43 | traces: 44 | receivers: [otlp] 45 | processors: [tail_sampling, batch] 46 | exporters: [otlp/tempo, otlp/jaeger] 47 | metrics: 48 | receivers: [otlp] 49 | exporters: [prometheus] 50 | processors: [batch] -------------------------------------------------------------------------------- /deployment/compose/prometheus/prometheus.yaml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s 3 | evaluation_interval: 15s 4 | 5 | scrape_configs: 6 | - job_name: 'prometheus' 7 | static_configs: 8 | - targets: ['localhost:9090'] 9 | - job_name: 'collector' 10 | static_configs: 11 | - targets: ['collector:9464'] 12 | - job_name: 'tempo' 13 | static_configs: 14 | - targets: ['tempo:3200'] 15 | - job_name: 'falco-talon' 16 | static_configs: 17 | - targets: ['host.docker.internal:2803'] -------------------------------------------------------------------------------- /deployment/compose/tempo/tempo.yaml: -------------------------------------------------------------------------------- 1 | stream_over_http_enabled: true 2 | server: 3 | http_listen_port: 3200 4 | log_level: info 5 | 6 | query_frontend: 7 | search: 8 | duration_slo: 5s 9 | throughput_bytes_slo: 1.073741824e+09 10 | trace_by_id: 11 | duration_slo: 5s 12 | 13 | distributor: 14 | receivers: # this configuration will listen on all ports and protocols that tempo is capable of. 15 | jaeger: # the receives all come from the OpenTelemetry collector. more configuration information can 16 | protocols: # be found there: https://github.com/open-telemetry/opentelemetry-collector/tree/main/receiver 17 | thrift_http: # 18 | grpc: # for a production deployment you should only enable the receivers you need! 19 | thrift_binary: 20 | thrift_compact: 21 | zipkin: 22 | otlp: 23 | protocols: 24 | http: 25 | grpc: 26 | opencensus: 27 | 28 | ingester: 29 | max_block_duration: 5m # cut the headblock when this much time passes. this is being set for demo purposes and should probably be left alone normally 30 | 31 | compactor: 32 | compaction: 33 | block_retention: 1h # overall Tempo trace retention. set for demo purposes 34 | 35 | metrics_generator: 36 | registry: 37 | external_labels: 38 | source: tempo 39 | cluster: docker-compose 40 | storage: 41 | path: /var/tempo/generator/wal 42 | remote_write: 43 | - url: http://prometheus:9090/api/v1/write 44 | send_exemplars: true 45 | traces_storage: 46 | path: /var/tempo/generator/traces 47 | 48 | storage: 49 | trace: 50 | backend: local # backend configuration to use 51 | wal: 52 | path: /var/tempo/wal # where to store the wal locally 53 | local: 54 | path: /var/tempo/blocks 55 | 56 | overrides: 57 | defaults: 58 | metrics_generator: 59 | processors: [service-graphs, span-metrics, local-blocks] # enables metrics generator 60 | generate_native_histograms: both -------------------------------------------------------------------------------- /examples.txt: -------------------------------------------------------------------------------- 1 | kubectl run test --image=alpine --restart=Never --command sleep infinity 2 | 3 | curl -XPOST "http://localhost:2803/" -d'{"output":"Test","priority":"Critical","Rule":"Test Terminate","time":"2019-05-17T15:31:56.746609046Z", "output_fields": {"k8s.pod.name":"test", "k8s.ns.name":"default"}}' 4 | 5 | curl -XPOST "http://localhost:2803/" -d'{"output":"Test","priority":"Warning","Rule":"Test Label","time":"2019-05-17T15:31:56.746609046Z", "output_fields": {"k8s.pod.name":"test", "k8s.ns.name":"default"}}' 6 | 7 | curl -XPOST "http://localhost:2803/" -d'{"output":"Test","priority":"Critical","Rule":"Test NetworkPolicy","time":"2019-05-17T15:31:56.746609046Z", "output_fields": {"k8s.pod.name":"static-web", "k8s.ns.name":"default", "fd.rip":"8.8.8.8", "fd.rport":"8888"}}' 8 | 9 | curl -XPOST "http://localhost:2803/" -d'{"output":"Test","priority":"Warning","Rule":"Test Exec","time":"2019-05-17T15:31:56.746609046Z", "output_fields": {"k8s.pod.name":"static-web", "k8s.ns.name":"default"}}' 10 | 11 | curl -XPOST "http://localhost:2803/" -d'{"output":"Test script","priority":"Warning","Rule":"Test Script","time":"2019-05-17T15:31:56.746609046Z", "output_fields": {"k8s.pod.name":"alpine", "k8s.ns.name":"default"}}' 12 | 13 | curl -XPOST "http://localhost:2803/" -d'{"output":"Test invoke lambda","priority":"Warning","Rule":"Test invoke lambda","time":"2019-05-17T15:31:56.746609046Z", "output_fields": {"lambda_name":"abcd"}}' -------------------------------------------------------------------------------- /internal/aws/checks/checks.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "context" 5 | 6 | awssdk "github.com/aws/aws-sdk-go-v2/aws" 7 | "github.com/aws/aws-sdk-go-v2/service/lambda" 8 | 9 | aws "github.com/falcosecurity/falco-talon/internal/aws/client" 10 | ) 11 | 12 | type CheckLambdaExist struct{} 13 | 14 | func (c CheckLambdaExist) Name() string { 15 | return "CheckLambdaExist" 16 | } 17 | 18 | func (c CheckLambdaExist) Run(functionName string) error { 19 | client := aws.GetLambdaClient() 20 | 21 | _, err := client.GetFunction(context.Background(), &lambda.GetFunctionInput{ 22 | FunctionName: awssdk.String(functionName), 23 | }) 24 | if err != nil { 25 | return err 26 | } 27 | return nil 28 | } 29 | 30 | func (c CheckLambdaExist) ListPermissions() string { 31 | return "permissions" 32 | } 33 | -------------------------------------------------------------------------------- /internal/aws/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/falcosecurity/falco-talon/configuration" 8 | "github.com/falcosecurity/falco-talon/utils" 9 | 10 | "github.com/aws/aws-sdk-go-v2/aws" 11 | "github.com/aws/aws-sdk-go-v2/config" 12 | "github.com/aws/aws-sdk-go-v2/credentials" 13 | "github.com/aws/aws-sdk-go-v2/credentials/stscreds" 14 | "github.com/aws/aws-sdk-go-v2/feature/ec2/imds" 15 | "github.com/aws/aws-sdk-go-v2/service/lambda" 16 | "github.com/aws/aws-sdk-go-v2/service/s3" 17 | "github.com/aws/aws-sdk-go-v2/service/sts" 18 | ) 19 | 20 | type AWSClient struct { 21 | lambdaClient *lambda.Client 22 | imdsClient *imds.Client 23 | s3Client *s3.Client 24 | cfg aws.Config 25 | } 26 | 27 | var ( 28 | awsClient *AWSClient 29 | once sync.Once 30 | ) 31 | 32 | func Init() error { 33 | if awsClient != nil { 34 | return nil 35 | } 36 | 37 | var initErr error 38 | 39 | once.Do(func() { 40 | awsConfig := configuration.GetConfiguration().AwsConfig 41 | var cfg aws.Config 42 | var err error 43 | 44 | if awsConfig.AccessKey != "" && awsConfig.SecretKey != "" && awsConfig.Region != "" { 45 | cfg, err = config.LoadDefaultConfig( 46 | context.TODO(), 47 | config.WithRegion(awsConfig.Region), 48 | config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(awsConfig.AccessKey, awsConfig.SecretKey, "")), 49 | ) 50 | } else { 51 | cfg, err = config.LoadDefaultConfig(context.TODO()) 52 | } 53 | if err != nil { 54 | initErr = err 55 | return 56 | } 57 | 58 | if awsConfig.RoleArn != "" { 59 | stsClient := sts.NewFromConfig(cfg) 60 | assumeRoleOptions := func(o *stscreds.AssumeRoleOptions) { 61 | if awsConfig.ExternalID != "" { 62 | o.ExternalID = aws.String(awsConfig.ExternalID) 63 | } 64 | } 65 | provider := stscreds.NewAssumeRoleProvider(stsClient, awsConfig.RoleArn, assumeRoleOptions) 66 | cfg.Credentials = aws.NewCredentialsCache(provider) 67 | } 68 | 69 | // Perform a dry run to validate credentials 70 | stsClient := sts.NewFromConfig(cfg) 71 | _, err = stsClient.GetCallerIdentity(context.TODO(), &sts.GetCallerIdentityInput{}) 72 | if err != nil { 73 | initErr = err 74 | return 75 | } 76 | 77 | awsClient = &AWSClient{ 78 | cfg: cfg, 79 | } 80 | 81 | if initErr == nil { 82 | utils.PrintLog(utils.InfoStr, utils.LogLine{Message: "init", Category: "aws", Status: utils.SuccessStr}) 83 | } 84 | }) 85 | 86 | return initErr 87 | } 88 | 89 | func GetAWSClient() *AWSClient { 90 | return awsClient 91 | } 92 | 93 | func GetLambdaClient() *lambda.Client { 94 | c := GetAWSClient() 95 | if c == nil { 96 | return nil 97 | } 98 | if c.lambdaClient == nil { 99 | c.lambdaClient = lambda.NewFromConfig(c.cfg) 100 | } 101 | return c.lambdaClient 102 | } 103 | 104 | func GetImdsClient() *imds.Client { 105 | c := GetAWSClient() 106 | if c == nil { 107 | return nil 108 | } 109 | if c.imdsClient == nil { 110 | c.imdsClient = imds.NewFromConfig(c.cfg) 111 | } 112 | return GetAWSClient().imdsClient 113 | } 114 | 115 | func GetS3Client() *s3.Client { 116 | c := GetAWSClient() 117 | if c == nil { 118 | return nil 119 | } 120 | if c.s3Client == nil { 121 | c.s3Client = s3.NewFromConfig(c.cfg) 122 | } 123 | return c.s3Client 124 | } 125 | 126 | func (client AWSClient) GetRegion() string { 127 | return client.cfg.Region 128 | } 129 | -------------------------------------------------------------------------------- /internal/calico/client/client.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | calico "github.com/projectcalico/api/pkg/client/clientset_generated/clientset" 5 | "k8s.io/client-go/rest" 6 | "k8s.io/client-go/tools/clientcmd" 7 | 8 | "github.com/falcosecurity/falco-talon/configuration" 9 | kubernetes "github.com/falcosecurity/falco-talon/internal/kubernetes/client" 10 | ) 11 | 12 | type Client struct { 13 | *calico.Clientset 14 | } 15 | 16 | var client *Client 17 | 18 | func Init() error { 19 | // the calico category requires also a k8s client 20 | if err := kubernetes.Init(); err != nil { 21 | return err 22 | } 23 | 24 | client = new(Client) 25 | config := configuration.GetConfiguration() 26 | var err error 27 | var restConfig *rest.Config 28 | if config.KubeConfig != "" { 29 | restConfig, err = clientcmd.BuildConfigFromFlags("", config.KubeConfig) 30 | } else { 31 | restConfig, err = rest.InClusterConfig() 32 | } 33 | if err != nil { 34 | return err 35 | } 36 | 37 | // creates the clientset 38 | client.Clientset, err = calico.NewForConfig(restConfig) 39 | if err != nil { 40 | return err 41 | } 42 | return nil 43 | } 44 | 45 | func GetClient() *Client { 46 | return client 47 | } 48 | -------------------------------------------------------------------------------- /internal/cilium/client/client.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | cilium "github.com/cilium/cilium/pkg/k8s/client/clientset/versioned" 5 | "k8s.io/client-go/rest" 6 | "k8s.io/client-go/tools/clientcmd" 7 | 8 | "github.com/falcosecurity/falco-talon/configuration" 9 | kubernetes "github.com/falcosecurity/falco-talon/internal/kubernetes/client" 10 | ) 11 | 12 | type Client struct { 13 | *cilium.Clientset 14 | } 15 | 16 | var client *Client 17 | 18 | func Init() error { 19 | // the calico category requires also a k8s client 20 | if err := kubernetes.Init(); err != nil { 21 | return err 22 | } 23 | 24 | client = new(Client) 25 | config := configuration.GetConfiguration() 26 | var err error 27 | var restConfig *rest.Config 28 | if config.KubeConfig != "" { 29 | restConfig, err = clientcmd.BuildConfigFromFlags("", config.KubeConfig) 30 | } else { 31 | restConfig, err = rest.InClusterConfig() 32 | } 33 | if err != nil { 34 | return err 35 | } 36 | 37 | // creates the clientset 38 | client.Clientset, err = cilium.NewForConfig(restConfig) 39 | if err != nil { 40 | return err 41 | } 42 | return nil 43 | } 44 | 45 | func GetClient() *Client { 46 | return client 47 | } 48 | -------------------------------------------------------------------------------- /internal/context/aws/aws.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "context" 5 | 6 | aws "github.com/falcosecurity/falco-talon/internal/aws/client" 7 | "github.com/falcosecurity/falco-talon/internal/events" 8 | ) 9 | 10 | func GetAwsContext(_ *events.Event) (map[string]any, error) { 11 | imdsClient := aws.GetImdsClient() 12 | 13 | info, err := imdsClient.GetIAMInfo(context.Background(), nil) 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | region, err := imdsClient.GetRegion(context.Background(), nil) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | elements := make(map[string]any) 24 | elements["aws.instance_profile_arn"] = info.InstanceProfileArn 25 | elements["aws.instance_profile_id"] = info.InstanceProfileID 26 | elements["aws.region"] = region.Region 27 | return elements, nil 28 | } 29 | -------------------------------------------------------------------------------- /internal/context/context.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "go.opentelemetry.io/otel/attribute" 9 | "go.opentelemetry.io/otel/codes" 10 | oteltrace "go.opentelemetry.io/otel/trace" 11 | 12 | "github.com/falcosecurity/falco-talon/internal/context/aws" 13 | "github.com/falcosecurity/falco-talon/internal/otlp/traces" 14 | 15 | "github.com/falcosecurity/falco-talon/internal/context/kubernetes" 16 | "github.com/falcosecurity/falco-talon/internal/events" 17 | ) 18 | 19 | func GetContext(actx context.Context, source string, event *events.Event) (map[string]any, error) { 20 | tracer := traces.GetTracer() 21 | 22 | _, span := tracer.Start(actx, "context", 23 | oteltrace.WithAttributes(attribute.String("context.source", source)), 24 | ) 25 | defer span.End() 26 | 27 | context := make(map[string]any) 28 | var err error 29 | 30 | switch source { 31 | case "aws": 32 | context, err = aws.GetAwsContext(event) 33 | case "k8snode": 34 | context, err = kubernetes.GetNodeContext(event) 35 | default: 36 | err = fmt.Errorf("unknown context '%v'", source) 37 | } 38 | 39 | if err != nil { 40 | span.SetStatus(codes.Error, "failed to add context") 41 | span.RecordError(err) 42 | return nil, err 43 | } 44 | 45 | for k, v := range context { 46 | span.SetAttributes(attribute.String(strings.ToLower(k), fmt.Sprintf("%v", v))) 47 | } 48 | 49 | return context, nil 50 | } 51 | -------------------------------------------------------------------------------- /internal/context/kubernetes/kubernetes.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "github.com/falcosecurity/falco-talon/internal/events" 5 | kubernetes "github.com/falcosecurity/falco-talon/internal/kubernetes/client" 6 | ) 7 | 8 | func GetNodeContext(event *events.Event) (map[string]any, error) { 9 | podName := event.GetPodName() 10 | namespace := event.GetNamespaceName() 11 | 12 | client := kubernetes.GetClient() 13 | pod, err := client.GetPod(podName, namespace) 14 | if err != nil { 15 | return nil, err 16 | } 17 | node, err := client.GetNodeFromPod(pod) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | elements := make(map[string]any) 23 | elements["node.hostname"] = node.Labels["kubernetes.io/hostname"] 24 | elements["node.instancetype"] = node.Labels["node.kubernetes.io/instance-type"] 25 | elements["node.role"] = node.Labels["kubernetes.io/role"] 26 | elements["node.topology.region"] = node.Labels["topology.kubernetes.io/region"] 27 | elements["node.topology.zone"] = node.Labels["topology.kubernetes.io/zone"] 28 | elements["node.spec.providerid"] = node.Spec.ProviderID 29 | 30 | return elements, nil 31 | } 32 | -------------------------------------------------------------------------------- /internal/events/events.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "os" 8 | "regexp" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | type Event struct { 14 | TraceID string `json:"trace_id"` 15 | Output string `json:"output"` 16 | Priority string `json:"priority"` 17 | Rule string `json:"rule"` 18 | Hostname string `json:"hostname"` 19 | Time time.Time `json:"time"` 20 | Source string `json:"source"` 21 | OutputFields map[string]any `json:"output_fields"` 22 | Context map[string]any `json:"context"` 23 | Tags []any `json:"tags"` 24 | } 25 | 26 | const ( 27 | trimPrefix = "(?i)^\\d{2}:\\d{2}:\\d{2}\\.\\d{9}\\:\\ (Debug|Info|Informational|Notice|Warning|Error|Critical|Alert|Emergency)" 28 | ) 29 | 30 | var regTrimPrefix *regexp.Regexp 31 | 32 | func init() { 33 | regTrimPrefix = regexp.MustCompile(trimPrefix) 34 | } 35 | 36 | func DecodeEvent(payload io.Reader) (*Event, error) { 37 | var event Event 38 | 39 | d := json.NewDecoder(payload) 40 | d.UseNumber() 41 | 42 | err := d.Decode(&event) 43 | if err != nil { 44 | return &Event{}, err 45 | } 46 | 47 | if event.Source == "" { 48 | event.Source = "syscall" 49 | } 50 | 51 | event.Output = regTrimPrefix.ReplaceAllString(event.Output, "") 52 | event.Output = strings.TrimPrefix(event.Output, " ") 53 | 54 | return &event, nil 55 | } 56 | 57 | func (event *Event) GetPodName() string { 58 | if event.OutputFields["k8s.pod.name"] != nil { 59 | return event.OutputFields["k8s.pod.name"].(string) 60 | } 61 | if event.OutputFields["ka.target.pod.name"] != nil { 62 | return event.OutputFields["ka.target.pod.name"].(string) 63 | } 64 | if event.OutputFields["ka.target.name"] != nil { 65 | return event.OutputFields["ka.target.name"].(string) 66 | } 67 | return "" 68 | } 69 | 70 | func (event *Event) GetNamespaceName() string { 71 | if event.OutputFields["k8s.ns.name"] != nil { 72 | return event.OutputFields["k8s.ns.name"].(string) 73 | } 74 | if event.OutputFields["ka.target.namespace"] != nil { 75 | return event.OutputFields["ka.target.namespace"].(string) 76 | } 77 | return "" 78 | } 79 | 80 | func (event *Event) GetHostname() string { 81 | return event.Hostname 82 | } 83 | 84 | func (event *Event) GetTargetName() string { 85 | if event.OutputFields["ka.target.name"] != nil { 86 | return event.OutputFields["ka.target.name"].(string) 87 | } 88 | return "" 89 | } 90 | 91 | func (event *Event) GetTargetNamespace() string { 92 | if event.OutputFields["ka.target.namespace"] != nil { 93 | return event.OutputFields["ka.target.namespace"].(string) 94 | } 95 | return "" 96 | } 97 | 98 | func (event *Event) GetTargetResource() string { 99 | if event.OutputFields["ka.target.resource"] != nil { 100 | return event.OutputFields["ka.target.resource"].(string) 101 | } 102 | return "" 103 | } 104 | 105 | func (event *Event) GetRemoteIP() string { 106 | if i := event.OutputFields["fd.rip"]; i != nil { 107 | return i.(string) 108 | } 109 | if i := event.OutputFields["fd.sip"]; i != nil { 110 | return i.(string) 111 | } 112 | return "" 113 | } 114 | 115 | func (event *Event) GetRemotePort() string { 116 | if i := event.OutputFields["fd.rport"]; i != nil { 117 | return i.(string) 118 | } 119 | if i := event.OutputFields["fd.sport"]; i != nil { 120 | return i.(string) 121 | } 122 | return "" 123 | } 124 | 125 | func (event *Event) GetRemoteProtocol() string { 126 | if i := event.OutputFields["fd.rproto"]; i != nil { 127 | return i.(string) 128 | } 129 | if i := event.OutputFields["fd.rproto"]; i != nil { 130 | return i.(string) 131 | } 132 | return "" 133 | } 134 | 135 | func (event *Event) AddContext(elements map[string]any) { 136 | if event.Context == nil { 137 | event.Context = make(map[string]any) 138 | } 139 | if len(elements) == 0 { 140 | return 141 | } 142 | for i, j := range elements { 143 | if fmt.Sprintf("%v", j) == "" { 144 | delete(elements, i) 145 | } 146 | } 147 | for i, j := range elements { 148 | event.Context[i] = j 149 | } 150 | } 151 | 152 | func (event *Event) ExportEnvVars() { 153 | for i, j := range event.OutputFields { 154 | key := strings.ReplaceAll(strings.ToUpper(i), ".", "_") 155 | key = strings.ReplaceAll(key, "[", "_") 156 | key = strings.ReplaceAll(key, "]", "") 157 | os.Setenv(key, fmt.Sprintf("%v", j)) 158 | } 159 | for i, j := range event.Context { 160 | key := strings.ReplaceAll(strings.ToUpper(i), ".", "_") 161 | os.Setenv(key, fmt.Sprintf("%v", j)) 162 | } 163 | os.Setenv("PRIORITY", event.Priority) 164 | os.Setenv("HOSTNAME", event.Hostname) 165 | os.Setenv("RULE", event.Rule) 166 | os.Setenv("SOURCE", event.Source) 167 | var tags []string 168 | for _, i := range event.Tags { 169 | tags = append(tags, fmt.Sprintf("%v", i)) 170 | } 171 | os.Setenv("TAGS", strings.Join(tags, ",")) 172 | } 173 | 174 | func (event *Event) String() string { 175 | e, _ := json.Marshal(*event) 176 | return string(e) 177 | } 178 | -------------------------------------------------------------------------------- /internal/gcp/checks/checks.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "context" 5 | 6 | "cloud.google.com/go/functions/apiv2/functionspb" 7 | 8 | "github.com/falcosecurity/falco-talon/internal/gcp/client" 9 | ) 10 | 11 | type CheckFunctionExist struct{} 12 | 13 | func (c CheckFunctionExist) Name() string { 14 | return "CheckFunctionExist" 15 | } 16 | 17 | func (c CheckFunctionExist) Run(functionName, location string) error { 18 | gcpClient, err := client.GetGCPClient() 19 | if err != nil { 20 | return err 21 | } 22 | 23 | functionClient, err := gcpClient.GetGcpFunctionClient(context.Background()) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | // Create a request to get function information 29 | req := &functionspb.GetFunctionRequest{ 30 | Name: "projects/" + gcpClient.ProjectID() + "/locations/" + location + "/functions/" + functionName, 31 | } 32 | 33 | _, err = functionClient.GetFunction(context.Background(), req) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /internal/gcp/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "sync" 8 | 9 | functionsv2 "cloud.google.com/go/functions/apiv2" 10 | "cloud.google.com/go/functions/apiv2/functionspb" 11 | "cloud.google.com/go/storage" 12 | "github.com/googleapis/gax-go/v2" 13 | "golang.org/x/oauth2/google" 14 | "google.golang.org/api/option" 15 | 16 | "github.com/falcosecurity/falco-talon/configuration" 17 | "github.com/falcosecurity/falco-talon/utils" 18 | ) 19 | 20 | const functionServiceScope = "https://www.googleapis.com/auth/cloud-platform" 21 | 22 | // nolint:govet 23 | type GCPClient struct { 24 | clientOpts []option.ClientOption 25 | functionsClient *functionsv2.FunctionClient 26 | storageClient *storage.Client 27 | httpClient HTTPClient 28 | projectID string 29 | functionsClientOnce sync.Once 30 | storageClientOnce sync.Once 31 | } 32 | 33 | type GCPClientAPI interface { 34 | GetGcpFunctionClient(context.Context) (*functionsv2.FunctionClient, error) 35 | GetStorageClient(context.Context) (*storage.Client, error) 36 | ProjectID() string 37 | HTTPClient() HTTPClient 38 | SetHTTPClient(httpClient HTTPClient) 39 | Close() []error 40 | } 41 | 42 | type HTTPClient interface { 43 | Do(req *http.Request) (*http.Response, error) 44 | } 45 | 46 | type GcpFunctionAPI interface { 47 | GetFunction(ctx context.Context, req *functionspb.GetFunctionRequest, opts ...gax.CallOption) (*functionspb.Function, error) 48 | Close() error 49 | } 50 | 51 | type GcpGcsAPI interface { 52 | Bucket(name string) *storage.BucketHandle 53 | Close() error 54 | } 55 | 56 | var ( 57 | gcpClient *GCPClient 58 | once sync.Once 59 | ) 60 | 61 | func Init() error { 62 | if gcpClient != nil { 63 | return nil 64 | } 65 | 66 | var initErr error 67 | once.Do(func() { 68 | gcpConfig := configuration.GetConfiguration().GcpConfig 69 | 70 | var clientOptions []option.ClientOption 71 | var creds *google.Credentials 72 | var err error 73 | 74 | if gcpConfig.CredentialsPath != "" { 75 | creds, err = google.CredentialsFromJSON(context.Background(), []byte(gcpConfig.CredentialsPath), functionServiceScope) 76 | if err != nil { 77 | initErr = fmt.Errorf("unable to load credentials from file: %v", err) 78 | return 79 | } 80 | clientOptions = append(clientOptions, option.WithCredentials(creds)) 81 | } else { 82 | creds, err = google.FindDefaultCredentials(context.Background(), functionServiceScope) 83 | if err != nil { 84 | initErr = fmt.Errorf("unable to find default credentials: %v", err) 85 | return 86 | } 87 | clientOptions = append(clientOptions, option.WithCredentials(creds)) 88 | } 89 | 90 | projectID, err := getProjectID(creds) 91 | if err != nil { 92 | initErr = err 93 | return 94 | } 95 | 96 | gcpClient = &GCPClient{ 97 | clientOpts: clientOptions, 98 | projectID: projectID, 99 | httpClient: &http.Client{}, 100 | } 101 | 102 | utils.PrintLog(utils.InfoStr, utils.LogLine{Message: "init", Category: "gcp", Status: utils.SuccessStr}) 103 | }) 104 | 105 | return initErr 106 | } 107 | 108 | func GetGCPClient() (*GCPClient, error) { 109 | if gcpClient == nil { 110 | err := Init() 111 | if err != nil { 112 | return nil, err 113 | } 114 | } 115 | return gcpClient, nil 116 | } 117 | 118 | func (c *GCPClient) GetGcpFunctionClient(ctx context.Context) (*functionsv2.FunctionClient, error) { 119 | var err error 120 | c.functionsClientOnce.Do(func() { 121 | c.functionsClient, err = functionsv2.NewFunctionClient(ctx, c.clientOpts...) 122 | }) 123 | if err != nil { 124 | return nil, err 125 | } 126 | return c.functionsClient, nil 127 | } 128 | 129 | func (c *GCPClient) GetStorageClient(ctx context.Context) (*storage.Client, error) { 130 | var err error 131 | c.storageClientOnce.Do(func() { 132 | c.storageClient, err = storage.NewClient(ctx, c.clientOpts...) 133 | }) 134 | if err != nil { 135 | return nil, err 136 | } 137 | return c.storageClient, nil 138 | } 139 | 140 | func (c *GCPClient) ProjectID() string { 141 | return c.projectID 142 | } 143 | 144 | func (c *GCPClient) HTTPClient() HTTPClient { 145 | return c.httpClient 146 | } 147 | 148 | // SetHTTPClient allows the user to set a custom HTTP client 149 | // to be used by the GCP client 150 | // this allows for better testing and control over the HTTP client 151 | func (c *GCPClient) SetHTTPClient(httpClient HTTPClient) { 152 | c.httpClient = httpClient 153 | } 154 | 155 | // Close at the main client level is responsible 156 | // for shutting down all the underlying service clients 157 | func (c *GCPClient) Close() []error { 158 | var errorList []error 159 | 160 | if c.functionsClient != nil { 161 | errorList = append(errorList, c.functionsClient.Close()) 162 | } 163 | if c.storageClient != nil { 164 | errorList = append(errorList, c.storageClient.Close()) 165 | } 166 | return errorList 167 | } 168 | 169 | func getProjectID(creds *google.Credentials) (string, error) { 170 | if creds.ProjectID == "" { 171 | return "", fmt.Errorf("project ID not available in the credentials, please specify your project ID in the GCP configuration") 172 | } 173 | return creds.ProjectID, nil 174 | } 175 | -------------------------------------------------------------------------------- /internal/handler/handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "crypto/md5" //nolint:gosec 5 | "encoding/hex" 6 | "fmt" 7 | "net/http" 8 | "strings" 9 | 10 | "go.opentelemetry.io/otel" 11 | "go.opentelemetry.io/otel/attribute" 12 | "go.opentelemetry.io/otel/codes" 13 | "go.opentelemetry.io/otel/propagation" 14 | "go.opentelemetry.io/otel/trace" 15 | 16 | "github.com/falcosecurity/falco-talon/configuration" 17 | "github.com/falcosecurity/falco-talon/internal/events" 18 | "github.com/falcosecurity/falco-talon/internal/nats" 19 | "github.com/falcosecurity/falco-talon/internal/otlp/metrics" 20 | "github.com/falcosecurity/falco-talon/internal/otlp/traces" 21 | "github.com/falcosecurity/falco-talon/utils" 22 | ) 23 | 24 | func MainHandler(w http.ResponseWriter, r *http.Request) { 25 | config := configuration.GetConfiguration() 26 | if r.Method != http.MethodPost { 27 | http.Error(w, "Please send with POST http method", http.StatusBadRequest) 28 | return 29 | } 30 | 31 | if r.Body == nil { 32 | http.Error(w, "Please send a valid request body", http.StatusBadRequest) 33 | return 34 | } 35 | 36 | event, err := events.DecodeEvent(r.Body) 37 | if err != nil { 38 | http.Error(w, "Please send a valid request body", http.StatusBadRequest) 39 | return 40 | } 41 | 42 | rctx := otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header)) 43 | 44 | tags := []string{} 45 | 46 | for _, i := range event.Tags { 47 | tags = append(tags, fmt.Sprintf("%v", i)) 48 | } 49 | 50 | tracer := traces.GetTracer() 51 | ctx, span := tracer.Start(rctx, "event", 52 | trace.WithAttributes(attribute.String("event.rule", event.Rule)), 53 | trace.WithAttributes(attribute.String("event.source", event.Source)), 54 | trace.WithAttributes(attribute.String("event.priority", event.Priority)), 55 | trace.WithAttributes(attribute.String("event.output", event.Output)), 56 | trace.WithAttributes(attribute.String("event.tags", strings.ReplaceAll(strings.Trim(fmt.Sprint(event.Tags), "[]"), " ", ", "))), 57 | trace.WithAttributes(attribute.StringSlice("event.tags", tags)), 58 | ) 59 | for i, j := range event.OutputFields { 60 | span.SetAttributes(attribute.String("event.output_fields[\""+i+"\"]", fmt.Sprintf("%v", j))) 61 | } 62 | defer span.End() 63 | event.TraceID = span.SpanContext().TraceID().String() 64 | span.AddEvent(event.String(), trace.EventOption(trace.WithTimestamp(event.Time))) 65 | span.SetAttributes(attribute.String("event.traceid", event.TraceID)) 66 | span.SetStatus(codes.Ok, "event received") 67 | 68 | log := utils.LogLine{ 69 | Message: "event", 70 | Event: event.Rule, 71 | Priority: event.Priority, 72 | Output: event.Output, 73 | Source: event.Source, 74 | TraceID: event.TraceID, 75 | } 76 | 77 | if config.PrintAllEvents { 78 | utils.PrintLog(utils.InfoStr, log) 79 | } 80 | 81 | metrics.IncreaseCounter(log) 82 | 83 | hasher := md5.New() //nolint:gosec 84 | hasher.Write([]byte(event.Output)) 85 | 86 | err = nats.GetPublisher().PublishMsg(ctx, hex.EncodeToString(hasher.Sum(nil)), event.String()) 87 | if err != nil { 88 | http.Error(w, "Internal Server Error", http.StatusInternalServerError) 89 | } 90 | } 91 | 92 | // HealthHandler is a simple handler to test if daemon is UP. 93 | func HealthHandler(w http.ResponseWriter, _ *http.Request) { 94 | w.Header().Add("Content-Type", "application/json") 95 | _, _ = w.Write([]byte(`{"status": "ok"}`)) 96 | } 97 | -------------------------------------------------------------------------------- /internal/kubernetes/checks/checks.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "strconv" 7 | 8 | "github.com/falcosecurity/falco-talon/internal/events" 9 | k8s "github.com/falcosecurity/falco-talon/internal/kubernetes/client" 10 | ) 11 | 12 | func CheckPodName(event *events.Event) error { 13 | pod := event.GetPodName() 14 | if pod == "" { 15 | return errors.New("missing pod name") 16 | } 17 | return nil 18 | } 19 | 20 | func CheckNamespace(event *events.Event) error { 21 | namespace := event.GetNamespaceName() 22 | if namespace == "" { 23 | return errors.New("missing namespace") 24 | } 25 | return nil 26 | } 27 | 28 | func CheckPodExist(event *events.Event) error { 29 | if err := CheckPodName(event); err != nil { 30 | return err 31 | } 32 | if err := CheckNamespace(event); err != nil { 33 | return err 34 | } 35 | 36 | client := k8s.GetClient() 37 | if client == nil { 38 | return errors.New("wrong k8s client") 39 | } 40 | _, err := client.GetPod(event.GetPodName(), event.GetNamespaceName()) 41 | return err 42 | } 43 | 44 | func CheckTargetName(event *events.Event) error { 45 | if event.OutputFields["ka.target.name"] == nil { 46 | return errors.New("missing target name (ka.target.name)") 47 | } 48 | return nil 49 | } 50 | 51 | func CheckTargetResource(event *events.Event) error { 52 | if event.OutputFields["ka.target.resource"] == nil { 53 | return errors.New("missing target resource (ka.target.resource)") 54 | } 55 | return nil 56 | } 57 | 58 | func CheckTargetNamespace(event *events.Event) error { 59 | switch event.OutputFields["ka.target.resource"] { 60 | case "namespaces": 61 | return nil 62 | case "clusterroles": 63 | return nil 64 | } 65 | if event.OutputFields["ka.target.namespace"] == nil { 66 | return errors.New("missing target namespace (ka.target.namespace)") 67 | } 68 | return nil 69 | } 70 | 71 | func CheckRemoteIP(event *events.Event) error { 72 | if event.OutputFields["fd.sip"] == nil && 73 | event.OutputFields["fd.rip"] == nil { 74 | return errors.New("missing IP field(s) (fd.sip or fd.rip)") 75 | } 76 | if event.OutputFields["fd.sip"] != nil { 77 | if net.ParseIP(event.OutputFields["fd.sip"].(string)) == nil { 78 | return errors.New("wrong value for fd.sip") 79 | } 80 | } 81 | if event.OutputFields["fd.rip"] != nil { 82 | if net.ParseIP(event.OutputFields["fd.rip"].(string)) == nil { 83 | return errors.New("wrong value for fd.rip") 84 | } 85 | } 86 | 87 | return nil 88 | } 89 | 90 | func CheckRemotePort(event *events.Event) error { 91 | if event.OutputFields["fd.sport"] == nil && 92 | event.OutputFields["fd.rport"] == nil { 93 | return errors.New("missing Port field(s) (fd.sport or fd.port)") 94 | } 95 | if event.OutputFields["fd.sport"] != nil { 96 | if _, err := strconv.ParseUint(event.GetRemotePort(), 0, 16); err != nil { 97 | return errors.New("wrong value for fd.sport") 98 | } 99 | } 100 | if event.OutputFields["fd.rport"] != nil { 101 | if _, err := strconv.ParseUint(event.GetRemotePort(), 0, 16); err != nil { 102 | return errors.New("wrong value for fd.rport") 103 | } 104 | } 105 | 106 | return nil 107 | } 108 | 109 | func CheckTargetExist(event *events.Event) error { 110 | if err := CheckTargetResource(event); err != nil { 111 | return err 112 | } 113 | if err := CheckTargetName(event); err != nil { 114 | return err 115 | } 116 | if err := CheckTargetNamespace(event); err != nil { 117 | return err 118 | } 119 | 120 | client := k8s.GetClient() 121 | if client == nil { 122 | return errors.New("wrong k8s client") 123 | } 124 | _, err := client.GetTarget(event.GetTargetResource(), event.GetTargetName(), event.GetTargetNamespace()) 125 | return err 126 | } 127 | -------------------------------------------------------------------------------- /internal/minio/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "sync" 5 | 6 | minio "github.com/minio/minio-go/v7" 7 | "github.com/minio/minio-go/v7/pkg/credentials" 8 | 9 | "github.com/falcosecurity/falco-talon/configuration" 10 | "github.com/falcosecurity/falco-talon/utils" 11 | ) 12 | 13 | type MinioClient struct { 14 | minioClient *minio.Client 15 | } 16 | 17 | var ( 18 | minioClient *MinioClient 19 | once sync.Once 20 | ) 21 | 22 | func Init() error { 23 | if minioClient != nil { 24 | return nil 25 | } 26 | 27 | var initErr error 28 | 29 | once.Do(func() { 30 | config := configuration.GetConfiguration().MinioConfig 31 | 32 | c, err := minio.New(config.Endpoint, &minio.Options{ 33 | Creds: credentials.NewStaticV4(config.AccessKey, config.SecretKey, ""), 34 | Secure: config.UseSSL, 35 | }) 36 | if err != nil { 37 | initErr = err 38 | return 39 | } 40 | 41 | minioClient = &MinioClient{ 42 | minioClient: c, 43 | } 44 | 45 | if initErr == nil { 46 | utils.PrintLog(utils.InfoStr, utils.LogLine{Message: "init", Category: "minio", Status: utils.SuccessStr}) 47 | } 48 | }) 49 | 50 | return initErr 51 | } 52 | 53 | func GetClient() *minio.Client { 54 | return minioClient.minioClient 55 | } 56 | -------------------------------------------------------------------------------- /internal/models/models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Information struct { 4 | FullName string `yaml:"fullname"` 5 | Name string `yaml:"name"` 6 | Category string `yaml:"category"` 7 | Description string `yaml:"description"` 8 | Source string `yaml:"source"` 9 | Permissions string `yaml:"permissions"` 10 | Example string `yaml:"example"` 11 | RequiredOutputFields []string `yaml:"required_output_fields"` 12 | Continue bool `yaml:"continue"` 13 | UseContext bool `yaml:"use_context"` 14 | AllowOutput bool `yaml:"allow_output"` 15 | RequireOutput bool `yaml:"require_output"` 16 | } 17 | 18 | type Data struct { 19 | Name string 20 | Objects map[string]string 21 | Bytes []byte 22 | } 23 | 24 | type Parameters any 25 | -------------------------------------------------------------------------------- /internal/nats/nats.go: -------------------------------------------------------------------------------- 1 | package nats 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "go.opentelemetry.io/otel" 9 | "go.opentelemetry.io/otel/propagation" 10 | 11 | // "github.com/nats-io/nats.go" 12 | 13 | natsserver "github.com/nats-io/nats-server/v2/server" 14 | nats "github.com/nats-io/nats.go" 15 | ) 16 | 17 | type Client struct { 18 | nats.JetStreamContext 19 | } 20 | 21 | const ( 22 | streamName = "EVENTS" 23 | streamSubjects = "EVENTS.*" 24 | ) 25 | 26 | type MessageWithContext struct { 27 | Ctx context.Context 28 | Data []byte 29 | } 30 | 31 | var consumer, publisher *Client 32 | 33 | func StartServer(timeWindow int) (*natsserver.Server, error) { 34 | ns, err := natsserver.NewServer( 35 | &natsserver.Options{ 36 | JetStream: true, 37 | // StoreDir: nats.MemoryStorage.String(), 38 | }) 39 | if err != nil { 40 | return nil, err 41 | } 42 | go ns.Start() 43 | 44 | if !ns.ReadyForConnections(3 * time.Second) { 45 | return nil, fmt.Errorf("connection timeout") 46 | } 47 | 48 | consumer = new(Client) 49 | publisher = new(Client) 50 | 51 | if err := consumer.SetJetStreamContext(nats.DefaultURL); err != nil { 52 | return nil, err 53 | } 54 | if err := publisher.SetJetStreamContext(nats.DefaultURL); err != nil { 55 | return nil, err 56 | } 57 | 58 | if err := consumer.createStream(timeWindow); err != nil { 59 | return nil, err 60 | } 61 | 62 | return ns, nil 63 | } 64 | 65 | func (client *Client) SetJetStreamContext(addr string) error { 66 | nc, err := nats.Connect(addr) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | jsc, err := nc.JetStream(nats.PublishAsyncMaxPending(256)) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | client.JetStreamContext = jsc 77 | return nil 78 | } 79 | 80 | func GetConsumer() *Client { 81 | return consumer 82 | } 83 | 84 | func GetPublisher() *Client { 85 | return publisher 86 | } 87 | 88 | func (client *Client) ConsumeMsg() (chan MessageWithContext, error) { 89 | c := make(chan MessageWithContext, 20) 90 | _, err := client.JetStreamContext.Subscribe(streamSubjects, func(m *nats.Msg) { 91 | propagator := otel.GetTextMapPropagator() 92 | ctx := propagator.Extract(context.Background(), propagation.HeaderCarrier(m.Header)) 93 | 94 | if err := m.Ack(); err != nil { 95 | return 96 | } 97 | c <- MessageWithContext{Data: m.Data, Ctx: ctx} 98 | }, 99 | nats.DeliverNew()) 100 | 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | return c, nil 106 | } 107 | 108 | func (client *Client) PublishMsg(ctx context.Context, id, msg string) error { 109 | natsMsg := &nats.Msg{ 110 | Subject: streamName + "." + id, 111 | Data: []byte(msg), 112 | Header: nats.Header{}, 113 | } 114 | 115 | propagator := otel.GetTextMapPropagator() 116 | propagator.Inject(ctx, propagation.HeaderCarrier(natsMsg.Header)) 117 | 118 | if _, err := client.JetStreamContext.PublishMsg(natsMsg, 119 | nats.MsgId(id), 120 | nats.RetryAttempts(3), 121 | nats.RetryWait(500*time.Millisecond)); err != nil { 122 | return err 123 | } 124 | return nil 125 | } 126 | 127 | func (client *Client) createStream(timeWindow int) error { 128 | stream, err := client.JetStreamContext.StreamInfo(streamName) 129 | if err != nil { 130 | if err != nats.ErrStreamNotFound { 131 | return err 132 | } 133 | } 134 | if stream == nil { 135 | _, err = client.JetStreamContext.AddStream(&nats.StreamConfig{ 136 | Name: streamName, 137 | Subjects: []string{streamSubjects}, 138 | Duplicates: time.Duration(timeWindow) * time.Second, 139 | MaxAge: time.Duration(timeWindow) * time.Second, 140 | MaxMsgsPerSubject: 1, 141 | Storage: nats.MemoryStorage, 142 | }) 143 | if err != nil { 144 | return err 145 | } 146 | } 147 | return nil 148 | } 149 | -------------------------------------------------------------------------------- /internal/otlp/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/prometheus/client_golang/prometheus/promhttp" 11 | "go.opentelemetry.io/otel/attribute" 12 | "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" 13 | "go.opentelemetry.io/otel/exporters/prometheus" 14 | "go.opentelemetry.io/otel/metric" 15 | sdk "go.opentelemetry.io/otel/sdk/metric" 16 | "go.opentelemetry.io/otel/sdk/resource" 17 | semconv "go.opentelemetry.io/otel/semconv/v1.24.0" 18 | 19 | "github.com/falcosecurity/falco-talon/configuration" 20 | "github.com/falcosecurity/falco-talon/utils" 21 | ) 22 | 23 | const meterName = "github.com/falcosecurity/falco-talon" 24 | const metricPrefix = "falcosecurity_falco_talon_" 25 | 26 | var ( 27 | eventCounters metric.Int64Counter 28 | matchCounters metric.Int64Counter 29 | actionCounters metric.Int64Counter 30 | notificationCounters metric.Int64Counter 31 | outputCounters metric.Int64Counter 32 | ) 33 | var ctx context.Context 34 | 35 | func Init() { 36 | ctx = context.Background() 37 | config := configuration.GetConfiguration() 38 | 39 | exporter, err := prometheus.New() 40 | if err != nil { 41 | utils.PrintLog(utils.FatalStr, utils.LogLine{Error: err.Error(), Message: "init", Category: "otlp"}) 42 | log.Fatal(err) 43 | } 44 | 45 | resources := resource.NewWithAttributes( 46 | semconv.SchemaURL, 47 | semconv.ServiceNameKey.String("falco-talon"), 48 | semconv.ServiceVersionKey.String(configuration.GetInfo().GitVersion), 49 | ) 50 | 51 | var metricOpts []sdk.Option 52 | 53 | if config.Otel.MetricsEnabled { 54 | otlpExporter, err2 := newOtlpMetricExporter(config) 55 | if err2 != nil { 56 | utils.PrintLog(utils.FatalStr, utils.LogLine{Error: err2.Error(), Message: "init", Category: "otlp"}) 57 | log.Fatal(err2) 58 | } 59 | metricOpts = append(metricOpts, sdk.WithReader(sdk.NewPeriodicReader(otlpExporter))) 60 | } 61 | 62 | metricOpts = append(metricOpts, sdk.WithReader(exporter)) 63 | metricOpts = append(metricOpts, sdk.WithResource(resources)) 64 | 65 | provider := sdk.NewMeterProvider(metricOpts...) 66 | 67 | meter := provider.Meter( 68 | meterName, 69 | metric.WithInstrumentationVersion(configuration.GetInfo().GitVersion), 70 | ) 71 | 72 | eventCounters, _ = meter.Int64Counter(metricPrefix+"events", metric.WithDescription("number of received events")) 73 | matchCounters, _ = meter.Int64Counter(metricPrefix+"matches", metric.WithDescription("number of matched events")) 74 | actionCounters, _ = meter.Int64Counter(metricPrefix+"actions", metric.WithDescription("number of actions")) 75 | notificationCounters, _ = meter.Int64Counter(metricPrefix+"notifications", metric.WithDescription("number of notifications")) 76 | outputCounters, _ = meter.Int64Counter(metricPrefix+"outputs", metric.WithDescription("number of outputs")) 77 | } 78 | 79 | func newOtlpMetricExporter(cfg *configuration.Configuration) (sdk.Exporter, error) { 80 | endpoint := fmt.Sprintf("%s:%s", configuration.GetConfiguration().Otel.CollectorEndpoint, configuration.GetConfiguration().Otel.CollectorPort) 81 | insecure := cfg.Otel.CollectorUseInsecureGrpc 82 | 83 | var otlpmetricgrpcOpts []otlpmetricgrpc.Option 84 | 85 | if insecure { 86 | otlpmetricgrpcOpts = append(otlpmetricgrpcOpts, otlpmetricgrpc.WithInsecure()) 87 | } 88 | 89 | otlpmetricgrpcOpts = append(otlpmetricgrpcOpts, otlpmetricgrpc.WithEndpoint(endpoint)) 90 | otlpmetricgrpcOpts = append(otlpmetricgrpcOpts, otlpmetricgrpc.WithTimeout(time.Duration(cfg.Otel.Timeout)*time.Second)) 91 | otlpmetricgrpcOpts = append(otlpmetricgrpcOpts, otlpmetricgrpc.WithRetry(otlpmetricgrpc.RetryConfig{ 92 | Enabled: true, 93 | MaxInterval: 2 * time.Second, 94 | MaxElapsedTime: 10 * time.Second, 95 | })) 96 | 97 | return otlpmetricgrpc.New(ctx, otlpmetricgrpcOpts...) 98 | } 99 | 100 | func IncreaseCounter(log utils.LogLine) { 101 | opts := getMeasurementOption(log) 102 | switch log.Message { 103 | case "event": 104 | eventCounters.Add(ctx, 1, opts) 105 | case "match": 106 | matchCounters.Add(ctx, 1, opts) 107 | case "action": 108 | actionCounters.Add(ctx, 1, opts) 109 | case "notification": 110 | notificationCounters.Add(ctx, 1, opts) 111 | case "output": 112 | outputCounters.Add(ctx, 1, opts) 113 | } 114 | } 115 | 116 | func getMeasurementOption(log utils.LogLine) metric.MeasurementOption { 117 | attrs := []attribute.KeyValue{} 118 | if log.Rule != "" { 119 | attrs = append(attrs, attribute.Key("rule").String(log.Rule)) 120 | } 121 | if log.Event != "" { 122 | attrs = append(attrs, attribute.Key("event").String(log.Event)) 123 | } 124 | if log.Priority != "" { 125 | attrs = append(attrs, attribute.Key("priority").String(log.Priority)) 126 | } 127 | if log.Source != "" { 128 | attrs = append(attrs, attribute.Key("source").String(log.Source)) 129 | } 130 | if log.Notifier != "" { 131 | attrs = append(attrs, attribute.Key("notifier").String(log.Notifier)) 132 | } 133 | if log.Actionner != "" { 134 | attrs = append(attrs, attribute.Key("actionner").String(log.Actionner)) 135 | } 136 | if log.Category != "" { 137 | attrs = append(attrs, attribute.Key("category").String(log.Category)) 138 | } 139 | if log.Action != "" { 140 | attrs = append(attrs, attribute.Key("action").String(log.Action)) 141 | } 142 | if log.Status != "" { 143 | attrs = append(attrs, attribute.Key("status").String(log.Status)) 144 | } 145 | if log.OutputTarget != "" { 146 | attrs = append(attrs, attribute.Key("target").String(log.OutputTarget)) 147 | } 148 | if len(log.Objects) > 0 { 149 | for i, j := range log.Objects { 150 | attrs = append(attrs, attribute.Key(i).String(j)) 151 | } 152 | } 153 | 154 | opts := metric.WithAttributes(attrs...) 155 | return opts 156 | } 157 | 158 | func Handler() http.Handler { 159 | return promhttp.Handler() 160 | } 161 | -------------------------------------------------------------------------------- /internal/otlp/traces/traces.go: -------------------------------------------------------------------------------- 1 | package traces 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "time" 9 | 10 | "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" 11 | "go.opentelemetry.io/otel/sdk/resource" 12 | semconv "go.opentelemetry.io/otel/semconv/v1.24.0" 13 | oteltrace "go.opentelemetry.io/otel/trace" 14 | 15 | "github.com/falcosecurity/falco-talon/utils" 16 | 17 | "github.com/falcosecurity/falco-talon/configuration" 18 | 19 | "go.opentelemetry.io/otel" 20 | "go.opentelemetry.io/otel/propagation" 21 | "go.opentelemetry.io/otel/sdk/trace" 22 | ) 23 | 24 | var tracer oteltrace.Tracer 25 | 26 | //nolint:nakedret 27 | func SetupOTelSDK(ctx context.Context) (shutdown func(context.Context) error, oerr error) { 28 | var err error 29 | var shutdownFuncs []func(context.Context) error 30 | shutdown = func(ctx context.Context) error { 31 | for _, fn := range shutdownFuncs { 32 | err = errors.Join(err, fn(ctx)) 33 | } 34 | shutdownFuncs = nil 35 | return err 36 | } 37 | 38 | handleErr := func(inErr error) { 39 | err = errors.Join(inErr, shutdown(ctx)) 40 | } 41 | 42 | prop := newPropagator() 43 | otel.SetTextMapPropagator(prop) 44 | 45 | tracerProvider, err := newTraceProvider() 46 | if err != nil { 47 | handleErr(err) 48 | return 49 | } 50 | shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown) 51 | otel.SetTracerProvider(tracerProvider) 52 | otel.SetErrorHandler(otel.ErrorHandlerFunc(func(err error) { 53 | utils.PrintLog(utils.ErrorStr, utils.LogLine{Error: err.Error(), Message: "otel"}) 54 | })) 55 | tracer = tracerProvider.Tracer("falco-talon") 56 | 57 | return 58 | } 59 | 60 | func newPropagator() propagation.TextMapPropagator { 61 | return propagation.NewCompositeTextMapPropagator( 62 | propagation.TraceContext{}, 63 | propagation.Baggage{}, 64 | ) 65 | } 66 | 67 | func newTraceProvider() (*trace.TracerProvider, error) { 68 | config := configuration.GetConfiguration() 69 | 70 | if !config.Otel.TracesEnabled { 71 | return trace.NewTracerProvider(), nil 72 | } 73 | 74 | traceExporter, err := newOtlpGrpcExporter(context.Background()) 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | traceProvidersOpts := []trace.TracerProviderOption{} 80 | res := newResource() 81 | if res != nil { 82 | traceProvidersOpts = append(traceProvidersOpts, trace.WithResource(res)) 83 | } 84 | 85 | traceProvidersOpts = append(traceProvidersOpts, trace.WithBatcher(traceExporter, 86 | trace.WithBatchTimeout(time.Second*5), 87 | trace.WithExportTimeout(time.Second*30), 88 | )) 89 | 90 | traceProvider := trace.NewTracerProvider(traceProvidersOpts...) 91 | 92 | return traceProvider, nil 93 | } 94 | 95 | func newOtlpGrpcExporter(ctx context.Context) (trace.SpanExporter, error) { 96 | config := configuration.GetConfiguration() 97 | endpoint := fmt.Sprintf("%s:%s", config.Otel.CollectorEndpoint, configuration.GetConfiguration().Otel.CollectorPort) 98 | insecure := configuration.GetConfiguration().Otel.CollectorUseInsecureGrpc 99 | 100 | opts := []otlptracegrpc.Option{ 101 | otlptracegrpc.WithEndpoint(endpoint), 102 | otlptracegrpc.WithTimeout(time.Duration(config.Otel.Timeout) * time.Second), 103 | otlptracegrpc.WithRetry(otlptracegrpc.RetryConfig{ 104 | Enabled: true, 105 | MaxInterval: 2 * time.Second, 106 | MaxElapsedTime: 10 * time.Second, 107 | }), 108 | } 109 | 110 | if insecure { 111 | opts = append(opts, otlptracegrpc.WithInsecure()) 112 | } 113 | 114 | exporter, err := otlptracegrpc.New(ctx, opts...) 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | return exporter, nil 120 | } 121 | 122 | func newResource() *resource.Resource { 123 | hostname, _ := os.Hostname() 124 | res, err := resource.New( 125 | context.Background(), 126 | resource.WithAttributes( 127 | semconv.ServiceNameKey.String("falco-talon"), 128 | semconv.ServiceVersionKey.String(configuration.GetInfo().GitVersion), 129 | semconv.ServiceInstanceID(hostname), 130 | ), 131 | ) 132 | if err != nil { 133 | return nil 134 | } 135 | return res 136 | } 137 | 138 | func GetTracer() oteltrace.Tracer { 139 | return tracer 140 | } 141 | -------------------------------------------------------------------------------- /internal/rules/priority.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/falcosecurity/falco-talon/utils" 7 | ) 8 | 9 | const ( 10 | Default = iota 11 | Debug 12 | Informational 13 | Notice 14 | Warning 15 | Error 16 | Critical 17 | Alert 18 | Emergency 19 | ) 20 | 21 | func getPriorityNumber(priority string) int { 22 | switch strings.ToLower(priority) { 23 | case "emergency": 24 | return Emergency 25 | case "alert": 26 | return Alert 27 | case "critical": 28 | return Critical 29 | case utils.ErrorStr: 30 | return Error 31 | case "warning": 32 | return Warning 33 | case "notice": 34 | return Notice 35 | case "informational": 36 | return Informational 37 | case "debug": 38 | return Debug 39 | default: 40 | return Default 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /mageFile.go: -------------------------------------------------------------------------------- 1 | //go:build mage 2 | // +build mage 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | 10 | "github.com/magefile/mage/mg" 11 | "github.com/magefile/mage/sh" 12 | ) 13 | 14 | const ( 15 | repoURL string = "github.com/falcosecurity/falco-talon" 16 | ) 17 | 18 | type Lint mg.Namespace 19 | type Build mg.Namespace 20 | type Push mg.Namespace 21 | type Release mg.Namespace 22 | 23 | // lint:run runs linter 24 | func (Lint) Run() error { 25 | if err := sh.RunV("golangci-lint", "--version"); err != nil { 26 | return err 27 | } 28 | if err := sh.RunV("golangci-lint", "run", "--timeout", "3m"); err != nil { 29 | return err 30 | } 31 | if err := sh.RunV("go", "mod", "tidy"); err != nil { 32 | return err 33 | } 34 | return nil 35 | } 36 | 37 | // lint:fix fixes linting issues 38 | func (Lint) Fix() error { 39 | if err := sh.RunV("golangci-lint", "run", "--fix"); err != nil { 40 | return err 41 | } 42 | return nil 43 | } 44 | 45 | // test runs tests 46 | func Test() error { 47 | return sh.RunV("go", "test", "./...", "-race") 48 | } 49 | 50 | // run runs the app (with 'auto' as first argument, air is used to auto reload the app at each change) 51 | func Run(autoreload string) error { 52 | if err := sh.RunV("air", "-v"); err != nil { 53 | return err 54 | } 55 | 56 | if autoreload == "auto" { 57 | return sh.RunV("air", "server", "-c", "config.yaml", "-r", "rules.yaml") 58 | } 59 | return sh.RunV("go", "run", "./...", "server", "-c", "config.yaml", "-r", "rules.yaml") 60 | } 61 | 62 | // build:local builds a binary 63 | func (Build) Local() error { 64 | ldFlags := generateLDFlags() 65 | 66 | return sh.RunV("go", "build", "-trimpath", "-ldflags", ldFlags, "-o", "falco-talon", ".") 67 | } 68 | 69 | // build:images builds the images and push them the local docker daemon 70 | func (Build) Images() error { 71 | exportLDFlags() 72 | return sh.RunV("ko", "build", "--local", "--bare", "--sbom=none", "--tags", getVersion(), "--tags", getCommit(), "--tags", "latest", 73 | repoURL) 74 | } 75 | 76 | // push:images builds the images and push them to the Dockerhub 77 | func (Push) Images() error { 78 | exportLDFlags() 79 | os.Setenv("KO_DOCKER_REPO", "falcosecurity/falco-talon") 80 | return sh.RunV("ko", "build", "--bare", "--sbom=none", "--tags", getVersion(), "--tags", getCommit(), "--tags", "latest", 81 | repoURL) 82 | } 83 | 84 | // release:snapshot creates a release with current commit 85 | func (Release) Snapshot() error { 86 | exportLDFlags() 87 | return sh.RunV("goreleaser", "release", "--clean", "--snapshot", "--skip=sbom", "--skip-publish") 88 | } 89 | 90 | // release:tag creates a release from current tag 91 | func (Release) Tag() error { 92 | mg.Deps(Test) 93 | 94 | exportLDFlags() 95 | return sh.RunV("goreleaser", "release", "--clean", "--skip=sign", "--skip=sbom") 96 | } 97 | 98 | // clean cleans temp folders 99 | func Clean() { 100 | files := []string{"falco-talon", "dist"} 101 | 102 | for _, file := range files { 103 | sh.Rm(file) 104 | } 105 | } 106 | 107 | // exportLDFlags export as env vars the flags for go build 108 | func exportLDFlags() { 109 | os.Setenv("LDFLAGS", generateLDFlags()) 110 | } 111 | 112 | // getVersion gets a description of the commit, e.g. v0.30.1 (latest) or v0.30.1-32-gfe72ff73 (canary) 113 | func getVersion() string { 114 | version, _ := sh.Output("git", "describe", "--tags", "--match=v*") 115 | if version != "" { 116 | return version 117 | } 118 | 119 | gitBranch, _ := sh.Output("git", "branch", "--show-current") 120 | 121 | // repo without any tags in it 122 | return gitBranch 123 | } 124 | 125 | // getCommit gets the hash of the current commit 126 | func getCommit() string { 127 | commit, _ := sh.Output("git", "rev-parse", "--short", "HEAD") 128 | return commit 129 | } 130 | 131 | // getGitState gets the state of the git repository 132 | func getGitState() string { 133 | _, err := sh.Output("git", "diff", "--quiet") 134 | if err != nil { 135 | return "dirty" 136 | } 137 | 138 | return "clean" 139 | } 140 | 141 | // getBuildDateTime gets the build date and time 142 | func getBuildDateTime() string { 143 | result, _ := sh.Output("git", "log", "-1", "--pretty=%ct") 144 | if result != "" { 145 | sourceDateEpoch := fmt.Sprintf("@%s", result) 146 | date, _ := sh.Output("date", "-u", "-d", sourceDateEpoch, "+%Y-%m-%dT%H:%M:%SZ") 147 | return date 148 | } 149 | 150 | date, _ := sh.Output("date", "+%Y-%m-%dT%H:%M:%SZ") 151 | return date 152 | } 153 | 154 | func generateLDFlags() string { 155 | pkg := repoURL + "/configuration" 156 | return fmt.Sprintf("-X %[1]s.GitVersion=%[2]s -X %[1]s.GitCommit=%[3]s -X %[1]s.GitTreeState=%[4]s -X %[1]s.BuildDate=%[5]s", pkg, getVersion(), getCommit(), getGitState(), getBuildDateTime()) 157 | } 158 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/falcosecurity/falco-talon/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /notifiers/elasticsearch/elasticsearch.go: -------------------------------------------------------------------------------- 1 | package elasticsearch 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | "time" 9 | 10 | "github.com/falcosecurity/falco-talon/internal/models" 11 | "github.com/falcosecurity/falco-talon/notifiers/http" 12 | "github.com/falcosecurity/falco-talon/utils" 13 | ) 14 | 15 | const ( 16 | Name string = "elasticsearch" 17 | Description string = "Send a log to Elasticsearch" 18 | Permissions string = "" 19 | Example string = `notifiers: 20 | elasticsearch: 21 | url: "http://localhost:9200" 22 | create_index_template: true 23 | number_of_shards: 1 24 | number_of_replicas: 1 25 | notifiers: 26 | slack: 27 | webhook_url: "https://hooks.slack.com/services/XXXX" 28 | icon: "https://upload.wikimedia.org/wikipedia/commons/2/26/Circaetus_gallicus_claw.jpg" 29 | username: "Falco Talon" 30 | footer: "https://github.com/falcosecurity/falco-talon" 31 | format: long 32 | ` 33 | ) 34 | 35 | const docType string = "/_doc" 36 | const indexTemplate string = "/_index_template/falco-talon" 37 | 38 | type Parameters struct { 39 | CustomHeaders map[string]string `field:"custom_headers"` 40 | URL string `field:"url" validate:"required"` 41 | User string `field:"user"` 42 | Password string `field:"password"` 43 | Suffix string `field:"suffix" default:"daily"` 44 | Index string `field:"index" default:"falco-talon"` 45 | NumberOfShards int `field:"number_of_shards" default:"3"` 46 | NumberOfReplicas int `field:"number_of_replicas" default:"3"` 47 | CreateIndexTemplate bool `field:"create_index_template" default:"true"` 48 | } 49 | 50 | var parameters *Parameters 51 | 52 | type Notifier struct{} 53 | 54 | func Register() *Notifier { 55 | return new(Notifier) 56 | } 57 | 58 | func (n Notifier) Init(fields map[string]any) error { 59 | parameters = new(Parameters) 60 | parameters = utils.SetFields(parameters, fields).(*Parameters) 61 | if err := checkParameters(parameters); err != nil { 62 | return err 63 | } 64 | if parameters.CreateIndexTemplate { 65 | client := http.NewClient("GET", "", "", parameters.CustomHeaders) 66 | if parameters.User != "" && parameters.Password != "" { 67 | client.SetBasicAuth(parameters.User, parameters.Password) 68 | } 69 | if err := client.Request(parameters.URL+indexTemplate, nil); err != nil { 70 | if err.Error() == "resource not found" { 71 | client.SetHTTPMethod("PUT") 72 | m := strings.ReplaceAll(mapping, "${SHARDS}", fmt.Sprintf("%v", parameters.NumberOfShards)) 73 | m = strings.ReplaceAll(m, "${REPLICAS}", fmt.Sprintf("%v", parameters.NumberOfReplicas)) 74 | j := make(map[string]any) 75 | if err := json.Unmarshal([]byte(m), &j); err != nil { 76 | return err 77 | } 78 | if err := client.Request(parameters.URL+indexTemplate, j); err != nil { 79 | return err 80 | } 81 | } 82 | } 83 | } 84 | return nil 85 | } 86 | 87 | func (n Notifier) Information() models.Information { 88 | return models.Information{ 89 | Name: Name, 90 | Description: Description, 91 | Permissions: Permissions, 92 | Example: Example, 93 | } 94 | } 95 | func (n Notifier) Parameters() models.Parameters { 96 | return Parameters{ 97 | Suffix: "daily", 98 | Index: "falco-talon", 99 | NumberOfShards: 3, 100 | NumberOfReplicas: 3, 101 | CreateIndexTemplate: true, 102 | } 103 | } 104 | 105 | func (n Notifier) Run(log utils.LogLine) error { 106 | client := http.DefaultClient() 107 | 108 | current := time.Now() 109 | var u string 110 | switch parameters.Suffix { 111 | case "none": 112 | u = parameters.URL + "/" + parameters.Index + docType 113 | case "monthly": 114 | u = parameters.URL + "/" + parameters.Index + "-" + current.Format("2006.01") + docType 115 | case "annually": 116 | u = parameters.URL + "/" + parameters.Index + "-" + current.Format("2006") + docType 117 | default: 118 | u = parameters.URL + "/" + parameters.Index + "-" + current.Format("2006.01.02") + docType 119 | } 120 | 121 | log.Time = time.Now().Format(time.RFC3339) 122 | 123 | if err := client.Request(u, log); err != nil { 124 | return err 125 | } 126 | 127 | return nil 128 | } 129 | 130 | func checkParameters(parameters *Parameters) error { 131 | if parameters.URL == "" { 132 | return errors.New("wrong `url` setting") 133 | } 134 | if parameters.NumberOfShards < 1 { 135 | return errors.New("wrong `number_of_shards` setting") 136 | } 137 | if parameters.NumberOfReplicas < 1 { 138 | return errors.New("wrong `number_of_replcicas` setting") 139 | } 140 | 141 | if err := http.CheckURL(parameters.URL); err != nil { 142 | return err 143 | } 144 | 145 | if err := utils.ValidateStruct(parameters); err != nil { 146 | return err 147 | } 148 | 149 | return nil 150 | } 151 | -------------------------------------------------------------------------------- /notifiers/elasticsearch/mapping.go: -------------------------------------------------------------------------------- 1 | package elasticsearch 2 | 3 | var mapping = ` 4 | { 5 | "index_patterns": ["falco-talon*"], 6 | "template": { 7 | "settings": { 8 | "number_of_shards": ${SHARDS}, 9 | "number_of_replicas": ${REPLICAS} 10 | }, 11 | "mappings": { 12 | "_source": { 13 | "enabled": true 14 | }, 15 | "properties": { 16 | "action": { 17 | "type": "text", 18 | "fields": { 19 | "keyword": { 20 | "type": "keyword", 21 | "ignore_above": 128 22 | } 23 | } 24 | }, 25 | "actionner": { 26 | "type": "text", 27 | "fields": { 28 | "keyword": { 29 | "type": "keyword", 30 | "ignore_above": 128 31 | } 32 | } 33 | }, 34 | utils.ErrorStr: { 35 | "type": "text", 36 | "fields": { 37 | "keyword": { 38 | "type": "keyword", 39 | "ignore_above": 256 40 | } 41 | } 42 | }, 43 | "event": { 44 | "type": "text", 45 | "fields": { 46 | "keyword": { 47 | "type": "keyword", 48 | "ignore_above": 1024 49 | } 50 | } 51 | }, 52 | "message": { 53 | "type": "text", 54 | "fields": { 55 | "keyword": { 56 | "type": "keyword", 57 | "ignore_above": 128 58 | } 59 | } 60 | }, 61 | "output": { 62 | "type": "text", 63 | "fields": { 64 | "keyword": { 65 | "type": "keyword", 66 | "ignore_above": 2048 67 | } 68 | } 69 | }, 70 | "result": { 71 | "type": "text", 72 | "fields": { 73 | "keyword": { 74 | "type": "keyword", 75 | "ignore_above": 2048 76 | } 77 | } 78 | }, 79 | "objects": { 80 | "properties": { 81 | "namespace": { 82 | "type": "text", 83 | "fields": { 84 | "keyword": { 85 | "type": "keyword", 86 | "ignore_above": 128 87 | } 88 | } 89 | }, 90 | "pod": { 91 | "type": "text", 92 | "fields": { 93 | "keyword": { 94 | "type": "keyword", 95 | "ignore_above": 128 96 | } 97 | } 98 | } 99 | } 100 | }, 101 | "rule": { 102 | "type": "text", 103 | "fields": { 104 | "keyword": { 105 | "type": "keyword", 106 | "ignore_above": 128 107 | } 108 | } 109 | }, 110 | "status": { 111 | "type": "text", 112 | "fields": { 113 | "keyword": { 114 | "type": "keyword", 115 | "ignore_above": 128 116 | } 117 | } 118 | }, 119 | "trace_id": { 120 | "type": "text", 121 | "fields": { 122 | "keyword": { 123 | "type": "keyword", 124 | "ignore_above": 256 125 | } 126 | } 127 | } 128 | } 129 | } 130 | }, 131 | "_meta": { 132 | "description": "index template for falco talon logs" 133 | } 134 | } 135 | ` 136 | -------------------------------------------------------------------------------- /notifiers/http/client.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | "net/url" 12 | "regexp" 13 | 14 | "github.com/falcosecurity/falco-talon/utils" 15 | ) 16 | 17 | var ErrHeaderMissing = errors.New("header missing") // ErrHeaderMissing = 400 18 | var ErrClientAuthenticationError = errors.New("authentication error") // ErrClientAuthenticationError = 401 19 | var ErrForbidden = errors.New("access denied") // ErrForbidden = 403 20 | var ErrNotFound = errors.New("resource not found") // ErrNotFound = 404 21 | var ErrUnprocessableEntityError = errors.New("wrong request") // ErrUnprocessableEntityError = 422 22 | var ErrTooManyRequest = errors.New("exceeding post rate limit") // ErrTooManyRequest = 429 23 | 24 | const DefaultContentType = "application/json; charset=utf-8" 25 | const DefaultHTTPMethod = "POST" 26 | const DefaultUserAgent = "falco-talon" 27 | 28 | type Client struct { 29 | Headers http.Header 30 | HTTPMethod string 31 | Compressed bool 32 | } 33 | 34 | func CheckURL(u string) error { 35 | reg := regexp.MustCompile(`(http)(s?)://.*`) 36 | if !reg.MatchString(u) { 37 | return errors.New("invalid url") 38 | } 39 | 40 | if _, err := url.ParseRequestURI(u); err != nil { 41 | return errors.New("invalid url") 42 | } 43 | 44 | _, err := url.Parse(u) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func DefaultClient() Client { 53 | h := http.Header{} 54 | h.Set("Content-Type", DefaultContentType) 55 | return Client{ 56 | HTTPMethod: "POST", 57 | Headers: h, 58 | } 59 | } 60 | 61 | func (c *Client) SetHTTPMethod(hm string) { 62 | c.HTTPMethod = hm 63 | } 64 | 65 | func (c *Client) SetContentType(ct string) { 66 | c.Headers.Set("Content-Type", ct) 67 | } 68 | 69 | func (c *Client) SetBasicAuth(user, password string) { 70 | c.Headers.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(user+":"+password))) 71 | } 72 | 73 | func (c *Client) SetHeader(key, value string) { 74 | c.Headers.Set(key, value) 75 | } 76 | 77 | func (c *Client) DeleteHeader(key string) { 78 | c.Headers.Del(key) 79 | } 80 | 81 | func NewClient(httpMethod, contentType, userAgent string, headers map[string]string) Client { 82 | h := http.Header{} 83 | if len(headers) != 0 { 84 | for i, j := range headers { 85 | h.Add(i, j) 86 | } 87 | } 88 | 89 | m := DefaultHTTPMethod 90 | if httpMethod != "" { 91 | m = httpMethod 92 | } 93 | 94 | a := DefaultUserAgent 95 | if userAgent != "" { 96 | a = userAgent 97 | } 98 | h.Set("User-Agent", a) 99 | 100 | ct := DefaultContentType 101 | if contentType != "" { 102 | ct = contentType 103 | } 104 | h.Set("Content-Type", ct) 105 | 106 | return Client{ 107 | HTTPMethod: m, 108 | Headers: h, 109 | } 110 | } 111 | 112 | func (c *Client) Request(u string, payload any) error { 113 | // defer + recover to catch panic if output doesn't respond 114 | defer func() { 115 | if err := recover(); err != nil { 116 | utils.PrintLog(utils.ErrorStr, utils.LogLine{Error: "recover"}) 117 | } 118 | }() 119 | 120 | body := new(bytes.Buffer) 121 | 122 | if c.HTTPMethod != "GET" { 123 | if err := json.NewEncoder(body).Encode(payload); err != nil { 124 | return err 125 | } 126 | } 127 | 128 | client := &http.Client{ 129 | Transport: http.DefaultTransport.(*http.Transport).Clone(), 130 | } 131 | 132 | req, err := http.NewRequest(c.HTTPMethod, u, body) 133 | if err != nil { 134 | return err 135 | } 136 | 137 | req.Header = c.Headers 138 | 139 | resp, err := client.Do(req) 140 | if err != nil { 141 | return err 142 | } 143 | defer resp.Body.Close() 144 | 145 | switch resp.StatusCode { 146 | case http.StatusOK, http.StatusCreated, http.StatusAccepted, http.StatusNoContent: // 200, 201, 202, 204 147 | return nil 148 | case http.StatusBadRequest: // 400 149 | bodyBytes, err := io.ReadAll(resp.Body) 150 | if err != nil { 151 | return ErrHeaderMissing 152 | } 153 | return fmt.Errorf("%v: %v", ErrHeaderMissing, string(bodyBytes)) 154 | case http.StatusUnauthorized: // 401 155 | return ErrClientAuthenticationError 156 | case http.StatusForbidden: // 403 157 | return ErrForbidden 158 | case http.StatusNotFound: // 404 159 | return ErrNotFound 160 | case http.StatusUnprocessableEntity: // 422 161 | return ErrUnprocessableEntityError 162 | case http.StatusTooManyRequests: // 429 163 | return ErrTooManyRequest 164 | default: 165 | return errors.New(resp.Status) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /notifiers/k8sevents/k8sevents.go: -------------------------------------------------------------------------------- 1 | package k8sevents 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "strings" 8 | 9 | corev1 "k8s.io/api/core/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | 12 | textTemplate "text/template" 13 | 14 | kubernetes "github.com/falcosecurity/falco-talon/internal/kubernetes/client" 15 | "github.com/falcosecurity/falco-talon/internal/models" 16 | "github.com/falcosecurity/falco-talon/utils" 17 | ) 18 | 19 | const ( 20 | Name string = "k8sevents" 21 | Description string = "Create a Kubernetes Event" 22 | Permissions string = `- apiGroups: 23 | - "" 24 | resources: 25 | - events 26 | verbs: 27 | - get 28 | - update 29 | - patch 30 | - create 31 | ` 32 | Example string = `` 33 | ) 34 | 35 | const ( 36 | falcoTalon string = "falco-talon" 37 | defaultStr string = "default" 38 | ) 39 | 40 | type Parameters struct{} 41 | 42 | var plaintextTmpl = `Status: {{ .Status }} 43 | Message: {{ .Message }} 44 | {{- if .Rule }} 45 | Rule: {{ .Rule }} 46 | {{- end }} 47 | {{- if .Action }} 48 | Action: {{ .Action }} 49 | {{- end }} 50 | {{- if .Actionner }} 51 | Actionner: {{ .Actionner }} 52 | {{- end }} 53 | {{- if .Event }} 54 | Event: {{ .Event }} 55 | {{- end }} 56 | {{- range $key, $value := .Objects }} 57 | {{ $key }}: {{ $value }} 58 | {{- end }} 59 | {{- if .Error }} 60 | Error: {{ .Error }} 61 | {{- end }} 62 | {{- if .Result }} 63 | Result: {{ .Result }} 64 | {{- end }} 65 | {{- if .Output }} 66 | Output: {{ .Output }} 67 | {{- end }} 68 | {{- if .OutputTarget }} 69 | OutputTarget: {{ .OutputTarget }} 70 | {{- end }} 71 | TraceID: {{ .TraceID }} 72 | ` 73 | 74 | type Notifier struct{} 75 | 76 | func Register() *Notifier { 77 | return new(Notifier) 78 | } 79 | 80 | func (n Notifier) Init(_ map[string]any) error { return nil } 81 | 82 | func (n Notifier) Information() models.Information { 83 | return models.Information{ 84 | Name: Name, 85 | Description: Description, 86 | Permissions: Permissions, 87 | Example: Example, 88 | } 89 | } 90 | func (n Notifier) Parameters() models.Parameters { return Parameters{} } 91 | 92 | func (n Notifier) Run(log utils.LogLine) error { 93 | var err error 94 | var message string 95 | ttmpl := textTemplate.New("message") 96 | ttmpl, err = ttmpl.Parse(plaintextTmpl) 97 | if err != nil { 98 | return err 99 | } 100 | var messageBuf bytes.Buffer 101 | err = ttmpl.Execute(&messageBuf, log) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | message = utils.RemoveSpecialCharacters(messageBuf.String()) 107 | 108 | if len(message) > 1024 { 109 | message = message[:1024] 110 | } 111 | 112 | client := kubernetes.GetClient() 113 | 114 | namespace := log.Objects["namespace"] 115 | ns, err := client.GetNamespace(namespace) 116 | if err != nil { 117 | namespace = defaultStr 118 | } 119 | if ns == nil { 120 | namespace = defaultStr 121 | } 122 | 123 | var reason string 124 | if log.Actionner != "" { 125 | reason = log.Actionner 126 | } 127 | if log.OutputTarget != "" { 128 | reason = log.OutputTarget 129 | } 130 | 131 | k8sevent := &corev1.Event{ 132 | TypeMeta: metav1.TypeMeta{ 133 | Kind: "Event", 134 | APIVersion: "v1beta1", 135 | }, 136 | ObjectMeta: metav1.ObjectMeta{ 137 | GenerateName: falcoTalon + "-", 138 | }, 139 | InvolvedObject: corev1.ObjectReference{ 140 | Kind: "Pod", 141 | Namespace: namespace, 142 | Name: log.Objects["pod"], 143 | }, 144 | Reason: fmt.Sprintf("%v:%v:%v:%v", falcoTalon, log.Message, reason, log.Status), 145 | Message: strings.ReplaceAll(message, `'`, `"`), 146 | Source: corev1.EventSource{ 147 | Component: falcoTalon, 148 | }, 149 | Type: corev1.EventTypeNormal, 150 | EventTime: metav1.NowMicro(), 151 | ReportingController: "falcosecurity.org/" + falcoTalon, 152 | ReportingInstance: falcoTalon, 153 | Action: reason, 154 | } 155 | _, err = client.Clientset.CoreV1().Events(namespace).Create(context.TODO(), k8sevent, metav1.CreateOptions{}) 156 | if err != nil { 157 | return err 158 | } 159 | return nil 160 | } 161 | -------------------------------------------------------------------------------- /notifiers/loki/loki.go: -------------------------------------------------------------------------------- 1 | package loki 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "github.com/falcosecurity/falco-talon/internal/models" 10 | "github.com/falcosecurity/falco-talon/notifiers/http" 11 | "github.com/falcosecurity/falco-talon/utils" 12 | ) 13 | 14 | const ( 15 | Name string = "loki" 16 | Description string = "Send a log to Loki over HTTP" 17 | Permissions string = "" 18 | Example string = `notifiers: 19 | loki: 20 | host_port: "https://lolcalhost:3100" 21 | user: "xxxxx" 22 | api_key: "xxxxx" 23 | ` 24 | ) 25 | 26 | type Parameters struct { 27 | CustomHeaders map[string]string `field:"custom_headers"` 28 | URL string `field:"url" validate:"required"` 29 | User string `field:"user"` 30 | APIKey string `field:"api_key"` 31 | Tenant string `field:"tenant"` 32 | } 33 | 34 | type Payload struct { 35 | Streams []Stream `json:"streams"` 36 | } 37 | 38 | type Stream struct { 39 | Stream map[string]string `json:"stream"` 40 | Values []Value `json:"values"` 41 | } 42 | 43 | type Value []string 44 | 45 | const contentType = "application/json" 46 | 47 | var parameters *Parameters 48 | 49 | type Notifier struct{} 50 | 51 | func Register() *Notifier { 52 | return new(Notifier) 53 | } 54 | 55 | func (n Notifier) Init(fields map[string]any) error { 56 | parameters = new(Parameters) 57 | parameters = utils.SetFields(parameters, fields).(*Parameters) 58 | if err := checkParameters(parameters); err != nil { 59 | return err 60 | } 61 | return nil 62 | } 63 | 64 | func (n Notifier) Information() models.Information { 65 | return models.Information{ 66 | Name: Name, 67 | Description: Description, 68 | Permissions: Permissions, 69 | Example: Example, 70 | } 71 | } 72 | func (n Notifier) Parameters() models.Parameters { 73 | return Parameters{} 74 | } 75 | 76 | func (n Notifier) Run(log utils.LogLine) error { 77 | if parameters.URL == "" { 78 | return errors.New("wrong `host_port` setting") 79 | } 80 | 81 | if err := http.CheckURL(parameters.URL); err != nil { 82 | return err 83 | } 84 | 85 | client := http.NewClient("", contentType, "", parameters.CustomHeaders) 86 | 87 | if parameters.User != "" && parameters.APIKey != "" { 88 | client.SetBasicAuth(parameters.User, parameters.APIKey) 89 | } 90 | 91 | if parameters.Tenant != "" { 92 | client.SetHeader("X-Scope-OrgID", parameters.Tenant) 93 | } 94 | 95 | err := client.Request(parameters.URL+"/loki/api/v1/push", NewPayload(log)) 96 | if err != nil { 97 | return err 98 | } 99 | return nil 100 | } 101 | 102 | func checkParameters(parameters *Parameters) error { 103 | if parameters.URL == "" { 104 | return errors.New("wrong `host_port` setting") 105 | } 106 | 107 | if err := utils.ValidateStruct(parameters); err != nil { 108 | return err 109 | } 110 | 111 | return nil 112 | } 113 | 114 | func NewPayload(log utils.LogLine) Payload { 115 | s := make(map[string]string) 116 | 117 | s["status"] = log.Status 118 | if log.Rule != "" { 119 | s["rule"] = strings.ReplaceAll(strings.ToLower(log.Rule), " ", "_") 120 | } 121 | if log.Action != "" { 122 | s["action"] = strings.ReplaceAll(strings.ToLower(log.Action), " ", "_") 123 | } 124 | if log.Actionner != "" { 125 | s["actionner"] = log.Actionner 126 | } 127 | if log.OutputTarget != "" { 128 | s["outputtarget"] = log.OutputTarget 129 | } 130 | s["message"] = log.Message 131 | s["traceid"] = log.TraceID 132 | 133 | for k, v := range log.Objects { 134 | s[strings.ToLower(k)] = v 135 | } 136 | 137 | var t string 138 | 139 | if log.Output != "" { 140 | t = log.Output 141 | } 142 | if log.Result != "" { 143 | t = log.Result 144 | } 145 | if log.Error != "" { 146 | t = log.Error 147 | } 148 | 149 | return Payload{Streams: []Stream{ 150 | { 151 | Stream: s, 152 | Values: []Value{[]string{ 153 | fmt.Sprintf("%v", time.Now().UnixNano()), 154 | t, 155 | }}, 156 | }, 157 | }} 158 | } 159 | -------------------------------------------------------------------------------- /notifiers/notifiers.go: -------------------------------------------------------------------------------- 1 | package notifiers 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "go.opentelemetry.io/otel/attribute" 8 | "go.opentelemetry.io/otel/codes" 9 | "go.opentelemetry.io/otel/trace" 10 | "golang.org/x/text/cases" 11 | "golang.org/x/text/language" 12 | 13 | "github.com/falcosecurity/falco-talon/configuration" 14 | "github.com/falcosecurity/falco-talon/internal/events" 15 | "github.com/falcosecurity/falco-talon/internal/models" 16 | "github.com/falcosecurity/falco-talon/internal/otlp/metrics" 17 | "github.com/falcosecurity/falco-talon/internal/otlp/traces" 18 | "github.com/falcosecurity/falco-talon/internal/rules" 19 | "github.com/falcosecurity/falco-talon/notifiers/elasticsearch" 20 | "github.com/falcosecurity/falco-talon/notifiers/k8sevents" 21 | "github.com/falcosecurity/falco-talon/notifiers/loki" 22 | "github.com/falcosecurity/falco-talon/notifiers/slack" 23 | "github.com/falcosecurity/falco-talon/notifiers/smtp" 24 | "github.com/falcosecurity/falco-talon/notifiers/webhook" 25 | "github.com/falcosecurity/falco-talon/utils" 26 | ) 27 | 28 | type Notifier interface { 29 | Init(fields map[string]any) error 30 | Run(log utils.LogLine) error 31 | Information() models.Information 32 | Parameters() models.Parameters 33 | } 34 | 35 | type Notifiers []Notifier 36 | 37 | var defaultNotifiers *Notifiers 38 | var enabledNotifiers *Notifiers 39 | 40 | func init() { 41 | defaultNotifiers = new(Notifiers) 42 | defaultNotifiers = ListDefaultNotifiers() 43 | enabledNotifiers = new(Notifiers) 44 | } 45 | 46 | func ListDefaultNotifiers() *Notifiers { 47 | if len(*defaultNotifiers) == 0 { 48 | defaultNotifiers.Add( 49 | k8sevents.Register(), 50 | slack.Register(), 51 | smtp.Register(), 52 | webhook.Register(), 53 | loki.Register(), 54 | elasticsearch.Register(), 55 | ) 56 | } 57 | return defaultNotifiers 58 | } 59 | 60 | func (notifiers *Notifiers) Add(notifier ...Notifier) { 61 | for _, i := range notifier { 62 | *notifiers = append(*notifiers, i) 63 | } 64 | } 65 | 66 | func GetNotifiers() *Notifiers { 67 | return enabledNotifiers 68 | } 69 | 70 | func (notifiers *Notifiers) FindNotifier(name string) Notifier { 71 | if notifiers == nil { 72 | return nil 73 | } 74 | 75 | for _, i := range *notifiers { 76 | if i.Information().Name == name { 77 | return i 78 | } 79 | } 80 | return nil 81 | } 82 | 83 | func Init() { 84 | config := configuration.GetConfiguration() 85 | 86 | specifiedNotifiers := map[string]bool{} 87 | 88 | for _, i := range config.ListDefaultNotifiers() { 89 | specifiedNotifiers[i] = true 90 | } 91 | rules := rules.GetRules() 92 | for _, i := range *rules { 93 | for _, j := range i.ListNotifiers() { 94 | specifiedNotifiers[j] = true 95 | } 96 | } 97 | 98 | for i := range specifiedNotifiers { 99 | for _, j := range *defaultNotifiers { 100 | if strings.ToLower(i) == j.Information().Name { 101 | if err := j.Init(config.Notifiers[i]); err != nil { 102 | utils.PrintLog(utils.ErrorStr, utils.LogLine{Message: "init", Error: err.Error(), Category: j.Information().Name, Status: utils.FailureStr}) 103 | continue 104 | } 105 | enabledNotifiers.Add(j) 106 | } 107 | } 108 | } 109 | } 110 | 111 | func Notify(actx context.Context, rule *rules.Rule, action *rules.Action, event *events.Event, log utils.LogLine) { 112 | config := configuration.GetConfiguration() 113 | 114 | if len(rule.Notifiers) == 0 && len(config.DefaultNotifiers) == 0 { 115 | return 116 | } 117 | 118 | enabledNotifiers := map[string]bool{} 119 | 120 | for _, i := range config.DefaultNotifiers { 121 | enabledNotifiers[i] = true 122 | } 123 | for _, i := range rule.Notifiers { 124 | enabledNotifiers[i] = true 125 | } 126 | 127 | logN := utils.LogLine{ 128 | Message: "notification", 129 | Rule: rule.GetName(), 130 | Action: action.GetName(), 131 | Actionner: action.GetActionner(), 132 | TraceID: event.TraceID, 133 | } 134 | 135 | logN.Stage = "action" 136 | if log.OutputTarget != "" { 137 | logN.OutputTarget = log.OutputTarget 138 | logN.Stage = "output" 139 | } 140 | 141 | obj := make(map[string]string, len(log.Objects)) 142 | for i, j := range log.Objects { 143 | obj[cases.Title(language.Und, cases.NoLower).String(strings.ToLower(i))] = j 144 | } 145 | log.Objects = obj 146 | 147 | for i := range enabledNotifiers { 148 | if n := ListDefaultNotifiers().FindNotifier(i); n != nil { 149 | logN.Notifier = i 150 | tracer := traces.GetTracer() 151 | _, span := tracer.Start(actx, "notification", 152 | trace.WithAttributes(attribute.String("notifier.name", n.Information().Name)), 153 | ) 154 | 155 | if err := n.Run(log); err != nil { 156 | span.SetStatus(codes.Error, err.Error()) 157 | span.RecordError(err) 158 | logN.Status = utils.FailureStr 159 | logN.Error = err.Error() 160 | utils.PrintLog(utils.ErrorStr, logN) 161 | metrics.IncreaseCounter(log) 162 | } else { 163 | span.SetStatus(codes.Ok, "notification successfully sent") 164 | logN.Status = utils.SuccessStr 165 | utils.PrintLog(utils.InfoStr, logN) 166 | metrics.IncreaseCounter(logN) 167 | } 168 | span.End() 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /notifiers/smtp/smtp.go: -------------------------------------------------------------------------------- 1 | package smtp 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "errors" 7 | "fmt" 8 | "strings" 9 | "time" 10 | 11 | textTemplate "text/template" 12 | 13 | sasl "github.com/emersion/go-sasl" 14 | gosmtp "github.com/emersion/go-smtp" 15 | 16 | "github.com/falcosecurity/falco-talon/internal/models" 17 | "github.com/falcosecurity/falco-talon/utils" 18 | ) 19 | 20 | const ( 21 | Name string = "smtp" 22 | Description string = "Send an email with SMTP" 23 | Permissions string = "" 24 | Example string = `notifiers: 25 | smtp: 26 | host_port: "localhost:1025" 27 | from: "falco@falcosecurity.org" 28 | to: "user@test.com, other@test.com" 29 | user: "xxxxx" 30 | password: "xxxxx" 31 | format: "html" 32 | tls: false 33 | ` 34 | ) 35 | 36 | const ( 37 | Red string = "#e20b0b" 38 | Green string = "#23ba47" 39 | Grey string = "#a4a8b1" 40 | Text string = "text" 41 | 42 | rfc2822 string = "Mon Jan 02 15:04:05 -0700 2006" 43 | ) 44 | 45 | type Parameters struct { 46 | HostPort string `field:"host_port" validate:"required"` 47 | User string `field:"user"` 48 | Password string `field:"password"` 49 | From string `field:"from" validate:"required"` 50 | To string `field:"to" validate:"required"` 51 | Format string `field:"format" default:"html"` 52 | TLS bool `field:"tls" default:"false"` 53 | } 54 | 55 | // Payload 56 | type Payload struct { 57 | From string 58 | To string 59 | Subject string 60 | Body string 61 | Mime string 62 | Date string 63 | } 64 | 65 | var parameters *Parameters 66 | 67 | type Notifier struct{} 68 | 69 | func Register() *Notifier { 70 | return new(Notifier) 71 | } 72 | 73 | func (n Notifier) Init(fields map[string]any) error { 74 | parameters = new(Parameters) 75 | parameters = utils.SetFields(parameters, fields).(*Parameters) 76 | if err := checkParameters(parameters); err != nil { 77 | return err 78 | } 79 | return nil 80 | } 81 | 82 | func (n Notifier) Information() models.Information { 83 | return models.Information{ 84 | Name: Name, 85 | Description: Description, 86 | Permissions: Permissions, 87 | Example: Example, 88 | } 89 | } 90 | func (n Notifier) Parameters() models.Parameters { 91 | return Parameters{ 92 | Format: "html", 93 | } 94 | } 95 | 96 | func (n Notifier) Run(log utils.LogLine) error { 97 | if parameters.HostPort == "" { 98 | return errors.New("wrong `host_port` setting") 99 | } 100 | 101 | payload, err := NewPayload(log) 102 | if err != nil { 103 | return err 104 | } 105 | err = Send(payload) 106 | if err != nil { 107 | return err 108 | } 109 | return nil 110 | } 111 | 112 | func checkParameters(parameters *Parameters) error { 113 | if parameters.HostPort == "" { 114 | return errors.New("wrong `host_port` setting") 115 | } 116 | 117 | if err := utils.ValidateStruct(parameters); err != nil { 118 | return err 119 | } 120 | 121 | return nil 122 | } 123 | 124 | func NewPayload(log utils.LogLine) (Payload, error) { 125 | subject := fmt.Sprintf("Subject: [falco-talon][%v][%v] ", log.Status, log.Message) 126 | if log.OutputTarget != "" { 127 | subject += fmt.Sprintf("OutputTarget '%v' ", log.OutputTarget) 128 | } 129 | if log.Action != "" { 130 | subject += fmt.Sprintf("Action '%v' ", log.Action) 131 | } 132 | if log.Rule != "" { 133 | subject += fmt.Sprintf("Rule '%v' ", log.Rule) 134 | } 135 | subject = strings.TrimSuffix(subject, " ") 136 | 137 | payload := Payload{ 138 | From: fmt.Sprintf("From: %v", parameters.From), 139 | To: fmt.Sprintf("To: %v", parameters.To), 140 | Subject: subject, 141 | Mime: "MIME-version: 1.0;", 142 | Date: "Date: " + time.Now().Format(rfc2822), 143 | } 144 | 145 | if parameters.Format != Text { 146 | payload.Mime += "\nContent-Type: multipart/alternative; boundary=4t74weu9byeSdJTM\n\n\n--4t74weu9byeSdJTM" 147 | } 148 | 149 | payload.Mime += "\nContent-Type: text/plain; charset=\"UTF-8\";\n\n" 150 | 151 | var err error 152 | 153 | ttmpl := textTemplate.New(Text) 154 | ttmpl, err = ttmpl.Parse(plaintextTmpl) 155 | if err != nil { 156 | return Payload{}, err 157 | } 158 | var outtext bytes.Buffer 159 | err = ttmpl.Execute(&outtext, log) 160 | if err != nil { 161 | return Payload{}, err 162 | } 163 | 164 | if parameters.Format == Text { 165 | return payload, nil 166 | } 167 | 168 | htmpl := textTemplate.New("html") 169 | htmpl, err = htmpl.Parse(htmlTmpl) 170 | if err != nil { 171 | return Payload{}, err 172 | } 173 | var outhtml bytes.Buffer 174 | log.Output = strings.ReplaceAll(utils.RemoveSpecialCharacters(log.Output), "\n", "
") 175 | err = htmpl.Execute(&outhtml, log) 176 | if err != nil { 177 | return Payload{}, err 178 | } 179 | 180 | payload.Body = fmt.Sprintf("%v\n%v\n%v\n%v\n%v\n%v\n\n%v", 181 | payload.From, 182 | payload.To, 183 | payload.Date, 184 | payload.Mime, 185 | outtext.String(), 186 | "--4t74weu9byeSdJTM\nContent-Type: text/html; charset=\"UTF-8\";", 187 | outhtml.String(), 188 | ) 189 | 190 | return payload, nil 191 | } 192 | 193 | func Send(payload Payload) error { 194 | to := strings.Split(strings.ReplaceAll(parameters.To, " ", ""), ",") 195 | auth := sasl.NewPlainClient("", parameters.User, parameters.Password) 196 | 197 | var smtpClient *gosmtp.Client 198 | var err error 199 | if parameters.TLS { 200 | tlsCfg := &tls.Config{ 201 | ServerName: strings.Split(parameters.HostPort, ":")[0], 202 | MinVersion: tls.VersionTLS12, 203 | } 204 | smtpClient, err = gosmtp.DialStartTLS(parameters.HostPort, tlsCfg) 205 | } else { 206 | smtpClient, err = gosmtp.Dial(parameters.HostPort) 207 | } 208 | if err != nil { 209 | return err 210 | } 211 | 212 | err = smtpClient.Auth(auth) 213 | if err != nil { 214 | return err 215 | } 216 | err = smtpClient.SendMail(parameters.From, to, strings.NewReader(payload.Body)) 217 | if err != nil { 218 | return err 219 | } 220 | return nil 221 | } 222 | -------------------------------------------------------------------------------- /notifiers/smtp/templates.go: -------------------------------------------------------------------------------- 1 | package smtp 2 | 3 | var plaintextTmpl = `Status: {{ .Status }} 4 | {{- if .Action }} 5 | Action: {{ .Action }} 6 | {{- end }} 7 | {{- if .Actionner }} 8 | Actionner: {{ .Actionner }} 9 | {{- end }} 10 | {{- if .Rule }} 11 | Rule: {{ .Rule }} 12 | {{- end }} 13 | {{- if .Event }} 14 | Event: {{ .Event }} 15 | {{- end }} 16 | Message: {{ .Message }} 17 | {{- range $key, $value := .Objects }} 18 | {{ $key }}: {{ $value }} 19 | {{- end }} 20 | {{- if .Error }} 21 | Error: {{ .Error }} 22 | {{- end }} 23 | {{- if .Result }} 24 | Result: {{ .Result }} 25 | {{- end }} 26 | {{- if .Output }} 27 | Output: 28 | {{ .Output }} 29 | {{- end }} 30 | {{- if .OutputTarget }} 31 | OutputTarget: {{ .OutputTarget }} 32 | {{- end }} 33 | Trace ID: {{ .TraceID }} 34 | ` 35 | 36 | var htmlTmpl = ` 37 | {{ $color := "#858585" }} 38 | {{ $prio := printf "%v" .Status }} 39 | {{ if eq $prio utils.SuccessStr }}{{ $color = "#23ba47" }}{{ end }} 40 | {{ if eq $prio utils.FailureStr }}{{ $color = "#e20b0b" }}{{ end }} 41 | {{ if eq $prio "ignored" }}{{ $color = "#a4a8b1" }}{{ end }} 42 | 43 | 44 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
56 |
57 | 58 | 59 | 60 | 61 | 62 | 63 |
{{ .Status }}
64 | 65 | 66 | {{ if .Action }} 67 | 68 | 69 | 70 | 71 | {{ end }} 72 | {{ if .Actionner }} 73 | 74 | 75 | 76 | 77 | {{ end }} 78 | {{ if .Rule }} 79 | 80 | 81 | 82 | 83 | {{ end }} 84 | {{ range $key, $value := .Objects }} 85 | 86 | 87 | 88 | 89 | {{ end }} 90 | {{ if .Event }} 91 | 92 | 93 | 94 | 95 | {{ end }} 96 | {{ if .OutputTarget }} 97 | 98 | 99 | 100 | 101 | {{ end }} 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | {{ if .Error }} 111 | 112 | 113 | 114 | 115 | {{ end }} 116 | {{ if .Result }} 117 | 118 | 119 | 120 | 121 | {{ end }} 122 | {{ if .Output }} 123 | 124 | 125 | 126 | 127 | {{ end }} 128 | 129 |
Action{{ .Action }}
Actionner{{ .Actionner }}
Rule{{ .Rule }}
{{ $key }}{{ $value }}
Event{{ .Event }}
Target{{ .OutputTarget }}
Message{{ .Message }}
Trace ID{{ .TraceID }}
Error{{ .Error }}
Result{{ .Result }}
Output{{ printf "%s" .Output }}
130 |
131 | 132 | --4t74weu9byeSdJTM--` 133 | -------------------------------------------------------------------------------- /notifiers/webhook/webhook.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/falcosecurity/falco-talon/internal/models" 7 | "github.com/falcosecurity/falco-talon/notifiers/http" 8 | "github.com/falcosecurity/falco-talon/utils" 9 | ) 10 | 11 | const ( 12 | Name string = "webhook" 13 | Description string = "Send a message to an HTTP endpoint" 14 | Permissions string = "" 15 | Example string = `notifiers: 16 | webhook: 17 | url: "http://xxxxx" 18 | http_method: "POST" 19 | user_agent: "Falco-Talon" 20 | content_type: "application/json; charset=utf-8" 21 | custom_headers: 22 | Authorization: "Bearer xxxxx" 23 | ` 24 | ) 25 | 26 | type Parameters struct { 27 | CustomHeaders map[string]string `field:"custom_headers"` 28 | URL string `field:"url"` 29 | HTTPMethod string `field:"http_method" default:"POST"` 30 | ContentType string `field:"content_type" default:"application/json; charset=utf-8"` 31 | UserAgent string `field:"user_agent" default:"falco-talon"` 32 | } 33 | 34 | var parameters *Parameters 35 | 36 | type Notifier struct{} 37 | 38 | func Register() *Notifier { 39 | return new(Notifier) 40 | } 41 | 42 | func (n Notifier) Init(fields map[string]any) error { 43 | parameters = new(Parameters) 44 | parameters = utils.SetFields(parameters, fields).(*Parameters) 45 | if err := checkParameters(parameters); err != nil { 46 | return err 47 | } 48 | return nil 49 | } 50 | 51 | func (n Notifier) Information() models.Information { 52 | return models.Information{ 53 | Name: Name, 54 | Description: Description, 55 | Permissions: Permissions, 56 | Example: Example, 57 | } 58 | } 59 | func (n Notifier) Parameters() models.Parameters { 60 | return Parameters{ 61 | HTTPMethod: "POST", 62 | ContentType: "application/json; charset=utf-8", 63 | UserAgent: "falco-talon", 64 | } 65 | } 66 | 67 | func (n Notifier) Run(log utils.LogLine) error { 68 | client := http.NewClient( 69 | parameters.HTTPMethod, 70 | parameters.ContentType, 71 | parameters.UserAgent, 72 | parameters.CustomHeaders, 73 | ) 74 | 75 | err := client.Request(parameters.URL, log) 76 | if err != nil { 77 | return err 78 | } 79 | return nil 80 | } 81 | 82 | func checkParameters(parameters *Parameters) error { 83 | if parameters.URL == "" { 84 | return errors.New("wrong `url` setting") 85 | } 86 | 87 | if err := http.CheckURL(parameters.URL); err != nil { 88 | return err 89 | } 90 | 91 | if err := utils.ValidateStruct(parameters); err != nil { 92 | return err 93 | } 94 | 95 | return nil 96 | } 97 | -------------------------------------------------------------------------------- /outputs/aws/s3/s3.go: -------------------------------------------------------------------------------- 1 | package s3 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "strings" 9 | "time" 10 | 11 | awssdk "github.com/aws/aws-sdk-go-v2/aws" 12 | "github.com/aws/aws-sdk-go-v2/service/s3" 13 | 14 | aws "github.com/falcosecurity/falco-talon/internal/aws/client" 15 | "github.com/falcosecurity/falco-talon/internal/models" 16 | "github.com/falcosecurity/falco-talon/internal/rules" 17 | "github.com/falcosecurity/falco-talon/utils" 18 | ) 19 | 20 | const ( 21 | Name string = "s3" 22 | Category string = "aws" 23 | Description string = "Store on AWS S3" 24 | Permissions string = `{ 25 | "Id": "Policy1724925555994", 26 | "Version": "2012-10-17", 27 | "Statement": [ 28 | { 29 | "Sid": "Stmt1724925537082", 30 | "Action": [ 31 | "s3:PutObject", 32 | "s3:PutObjectAcl" 33 | ], 34 | "Effect": "Allow", 35 | "Resource": [ 36 | "arn:aws:s3:::${BucketName}/", 37 | "arn:aws:s3:::${BucketName}/*" 38 | ] 39 | } 40 | ] 41 | }` 42 | Example string = `- action: Get logs of the pod 43 | actionner: kubernetes:download 44 | parameters: 45 | tail_lines: 200 46 | output: 47 | target: aws:s3 48 | parameters: 49 | bucket: falco-talon 50 | prefix: files 51 | region: eu-west-1 52 | ` 53 | ) 54 | 55 | type Parameters struct { 56 | Bucket string `mapstructure:"bucket" validate:"required"` 57 | Prefix string `mapstructure:"prefix" validate:""` 58 | Region string `mapstructure:"region" validate:""` 59 | } 60 | 61 | type Output struct{} 62 | 63 | func Register() *Output { 64 | return new(Output) 65 | } 66 | 67 | func (o Output) Init() error { return aws.Init() } 68 | 69 | func (o Output) Information() models.Information { 70 | return models.Information{ 71 | Name: Name, 72 | FullName: Category + ":" + Name, 73 | Category: Category, 74 | Description: Description, 75 | Permissions: Permissions, 76 | Example: Example, 77 | } 78 | } 79 | func (o Output) Parameters() models.Parameters { 80 | return Parameters{ 81 | Prefix: "", 82 | Bucket: "", 83 | } 84 | } 85 | 86 | func (o Output) Checks(_ *rules.Output) error { return nil } 87 | 88 | func (o Output) Run(output *rules.Output, data *models.Data) (utils.LogLine, error) { 89 | var parameters Parameters 90 | err := utils.DecodeParams(output.GetParameters(), ¶meters) 91 | if err != nil { 92 | return utils.LogLine{ 93 | Objects: nil, 94 | Error: err.Error(), 95 | Status: utils.FailureStr, 96 | }, err 97 | } 98 | 99 | parameters.Prefix = strings.TrimSuffix(parameters.Prefix, "/") 100 | parameters.Prefix = strings.TrimPrefix(parameters.Prefix, "/") + "/" 101 | 102 | var key string 103 | switch { 104 | case data.Objects["namespace"] != "" && data.Objects["pod"] != "": 105 | key = fmt.Sprintf("%v_%v_%v_%v", time.Now().Format("2006-01-02T15-04-05Z"), data.Objects["namespace"], data.Objects["pod"], strings.ReplaceAll(data.Name, "/", "_")) 106 | case data.Objects["hostname"] != "": 107 | key = fmt.Sprintf("%v_%v_%v", time.Now().Format("2006-01-02T15-04-05Z"), data.Objects["hostname"], strings.ReplaceAll(data.Name, "/", "_")) 108 | default: 109 | var s string 110 | for i, j := range data.Objects { 111 | if i != "file" { 112 | s += j + "_" 113 | } 114 | } 115 | key = fmt.Sprintf("%v_%v%v", time.Now().Format("2006-01-02T15-04-05Z"), s, strings.ReplaceAll(data.Name, "/", "_")) 116 | } 117 | 118 | var region string 119 | awsClient := aws.GetAWSClient() 120 | if awsClient != nil { 121 | region = awsClient.GetRegion() 122 | } 123 | if parameters.Region != "" { 124 | region = parameters.Region 125 | } 126 | 127 | objects := map[string]string{ 128 | "file": data.Name, 129 | "bucket": parameters.Bucket, 130 | "prefix": parameters.Prefix, 131 | "key": key, 132 | "region": region, 133 | } 134 | 135 | if err := putObject(region, parameters.Bucket, parameters.Prefix, key, *data); err != nil { 136 | return utils.LogLine{ 137 | Objects: objects, 138 | Error: err.Error(), 139 | Status: utils.FailureStr, 140 | }, err 141 | } 142 | 143 | return utils.LogLine{ 144 | Objects: objects, 145 | Output: fmt.Sprintf("the file '%v' has been uploaded as the key '%v' to the bucket '%v'", data.Name, parameters.Prefix+key, parameters.Bucket), 146 | Status: utils.SuccessStr, 147 | }, nil 148 | } 149 | 150 | func (o Output) CheckParameters(output *rules.Output) error { 151 | var parameters Parameters 152 | 153 | err := utils.DecodeParams(output.GetParameters(), ¶meters) 154 | if err != nil { 155 | return err 156 | } 157 | 158 | err = utils.ValidateStruct(parameters) 159 | if err != nil { 160 | return err 161 | } 162 | 163 | return nil 164 | } 165 | 166 | func putObject(region, bucket, prefix, key string, data models.Data) error { 167 | client := aws.GetS3Client() 168 | if client == nil { 169 | return errors.New("client error") 170 | } 171 | 172 | ctx := context.Background() 173 | body := bytes.NewReader(data.Bytes) 174 | 175 | opts := func(o *s3.Options) { 176 | o.Region = region 177 | } 178 | 179 | _, err := client.PutObject( 180 | ctx, 181 | &s3.PutObjectInput{ 182 | Bucket: awssdk.String(bucket), 183 | Key: awssdk.String(prefix + key), 184 | Body: body, 185 | }, 186 | opts, 187 | ) 188 | if err != nil { 189 | return err 190 | } 191 | return nil 192 | } 193 | -------------------------------------------------------------------------------- /outputs/file/file.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "time" 9 | 10 | "github.com/falcosecurity/falco-talon/internal/models" 11 | "github.com/falcosecurity/falco-talon/internal/rules" 12 | "github.com/falcosecurity/falco-talon/utils" 13 | ) 14 | 15 | const ( 16 | Name string = "file" 17 | Category string = "local" 18 | Description string = "Store on local file system" 19 | Permissions string = `` 20 | Example string = `- action: Get logs of the pod 21 | actionner: kubernetes:download 22 | parameters: 23 | tail_lines: 200 24 | output: 25 | target: local:file 26 | parameters: 27 | destination: /var/logs/falco-talon/ 28 | ` 29 | ) 30 | 31 | type Parameters struct { 32 | Destination string `mapstructure:"destination" validate:"required"` 33 | } 34 | 35 | type Output struct{} 36 | 37 | func Register() *Output { 38 | return new(Output) 39 | } 40 | 41 | func (o Output) Init() error { return nil } 42 | 43 | func (o Output) Information() models.Information { 44 | return models.Information{ 45 | Name: Name, 46 | FullName: Category + ":" + Name, 47 | Category: Category, 48 | Description: Description, 49 | Permissions: Permissions, 50 | Example: Example, 51 | } 52 | } 53 | func (o Output) Parameters() models.Parameters { 54 | return Parameters{ 55 | Destination: "", 56 | } 57 | } 58 | 59 | func (o Output) Checks(output *rules.Output) error { 60 | var parameters Parameters 61 | err := utils.DecodeParams(output.GetParameters(), ¶meters) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | dstFolder := os.ExpandEnv(parameters.Destination) 67 | if _, err := os.Open(dstFolder); os.IsNotExist(err) { 68 | return fmt.Errorf("folder '%v' does not exist", dstFolder) 69 | } 70 | 71 | return nil 72 | } 73 | 74 | func (o Output) Run(output *rules.Output, data *models.Data) (utils.LogLine, error) { 75 | var parameters Parameters 76 | err := utils.DecodeParams(output.GetParameters(), ¶meters) 77 | if err != nil { 78 | return utils.LogLine{ 79 | Objects: nil, 80 | Error: err.Error(), 81 | Status: utils.FailureStr, 82 | }, err 83 | } 84 | 85 | var key string 86 | switch { 87 | case data.Objects["namespace"] != "" && data.Objects["pod"] != "": 88 | key = fmt.Sprintf("%v_%v_%v_%v", time.Now().Format("2006-01-02T15-04-05Z"), data.Objects["namespace"], data.Objects["pod"], strings.ReplaceAll(data.Name, "/", "_")) 89 | case data.Objects["hostname"] != "": 90 | key = fmt.Sprintf("%v_%v_%v", time.Now().Format("2006-01-02T15-04-05Z"), data.Objects["hostname"], strings.ReplaceAll(data.Name, "/", "_")) 91 | default: 92 | var s string 93 | for i, j := range data.Objects { 94 | if i != "file" { 95 | s += j + "_" 96 | } 97 | } 98 | key = fmt.Sprintf("%v_%v%v", time.Now().Format("2006-01-02T15-04-05Z"), s, strings.ReplaceAll(data.Name, "/", "_")) 99 | } 100 | 101 | dstfile := fmt.Sprintf("%v/%v", strings.TrimSuffix(parameters.Destination, "/"), key) 102 | 103 | objects := map[string]string{ 104 | "file": data.Name, 105 | "destination": dstfile, 106 | } 107 | 108 | if err := os.WriteFile(dstfile, data.Bytes, 0600); err != nil { 109 | return utils.LogLine{ 110 | Objects: objects, 111 | Error: err.Error(), 112 | Status: utils.FailureStr, 113 | }, err 114 | } 115 | 116 | return utils.LogLine{ 117 | Objects: objects, 118 | Output: fmt.Sprintf("the file '%v' has been copied to '%v'", filepath.Base(data.Name), dstfile), 119 | Status: utils.SuccessStr, 120 | }, nil 121 | } 122 | 123 | func (o Output) CheckParameters(output *rules.Output) error { 124 | var parameters Parameters 125 | err := utils.DecodeParams(output.GetParameters(), ¶meters) 126 | if err != nil { 127 | return err 128 | } 129 | 130 | err = utils.ValidateStruct(parameters) 131 | if err != nil { 132 | return err 133 | } 134 | 135 | return nil 136 | } 137 | -------------------------------------------------------------------------------- /outputs/gcs/gcs.go: -------------------------------------------------------------------------------- 1 | package gcs 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "github.com/falcosecurity/falco-talon/internal/gcp/client" 10 | "github.com/falcosecurity/falco-talon/internal/models" 11 | "github.com/falcosecurity/falco-talon/internal/rules" 12 | "github.com/falcosecurity/falco-talon/utils" 13 | ) 14 | 15 | const ( 16 | Name string = "gcs" 17 | Category string = "gcp" 18 | Description string = "Store on GCP Cloud Storage" 19 | Permissions string = `Required IAM permissions for GCS: 20 | - storage.objects.create 21 | - storage.objects.get 22 | - storage.objects.list 23 | - storage.objects.update` 24 | Example string = `- action: Get logs of the pod 25 | actionner: kubernetes:download 26 | parameters: 27 | tail_lines: 200 28 | output: 29 | target: gcp:gcs 30 | parameters: 31 | bucket: falco-talon 32 | prefix: files 33 | ` 34 | ) 35 | 36 | type Parameters struct { 37 | Bucket string `mapstructure:"bucket" validate:"required"` 38 | Prefix string `mapstructure:"prefix" validate:""` 39 | } 40 | 41 | type Output struct{} 42 | 43 | func Register() *Output { 44 | return new(Output) 45 | } 46 | 47 | func (o Output) Init() error { 48 | return client.Init() 49 | } 50 | 51 | func (o Output) Information() models.Information { 52 | return models.Information{ 53 | Name: Name, 54 | FullName: Category + ":" + Name, 55 | Category: Category, 56 | Description: Description, 57 | Permissions: Permissions, 58 | Example: Example, 59 | } 60 | } 61 | 62 | func (o Output) Parameters() models.Parameters { 63 | return Parameters{ 64 | Prefix: "", 65 | Bucket: "", 66 | } 67 | } 68 | 69 | func (o Output) Checks(_ *rules.Output) error { return nil } 70 | 71 | func (o Output) Run(output *rules.Output, data *models.Data) (utils.LogLine, error) { 72 | gcpClient, err := client.GetGCPClient() 73 | if err != nil { 74 | return utils.LogLine{ 75 | Objects: nil, 76 | Error: err.Error(), 77 | Status: utils.FailureStr, 78 | }, err 79 | } 80 | 81 | storageClient, err := gcpClient.GetStorageClient(context.Background()) 82 | if err != nil { 83 | return utils.LogLine{ 84 | Objects: nil, 85 | Error: err.Error(), 86 | Status: utils.FailureStr, 87 | }, err 88 | } 89 | return o.RunWithClient(storageClient, output, data) 90 | } 91 | 92 | func (o Output) CheckParameters(output *rules.Output) error { 93 | var parameters Parameters 94 | 95 | err := utils.DecodeParams(output.GetParameters(), ¶meters) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | err = utils.ValidateStruct(parameters) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | return nil 106 | } 107 | 108 | func (o Output) RunWithClient(client client.GcpGcsAPI, output *rules.Output, data *models.Data) (utils.LogLine, error) { 109 | var parameters Parameters 110 | err := utils.DecodeParams(output.GetParameters(), ¶meters) 111 | if err != nil { 112 | return utils.LogLine{ 113 | Objects: nil, 114 | Error: err.Error(), 115 | Status: utils.FailureStr, 116 | }, err 117 | } 118 | 119 | parameters.Prefix = strings.TrimSuffix(parameters.Prefix, "/") 120 | if parameters.Prefix != "" { 121 | parameters.Prefix += "/" 122 | } 123 | 124 | var key string 125 | switch { 126 | case data.Objects["namespace"] != "" && data.Objects["pod"] != "": 127 | key = fmt.Sprintf("%v_%v_%v_%v", time.Now().Format("2006-01-02T15-04-05Z"), data.Objects["namespace"], data.Objects["pod"], strings.ReplaceAll(data.Name, "/", "_")) 128 | case data.Objects["hostname"] != "": 129 | key = fmt.Sprintf("%v_%v_%v", time.Now().Format("2006-01-02T15-04-05Z"), data.Objects["hostname"], strings.ReplaceAll(data.Name, "/", "_")) 130 | default: 131 | var s string 132 | for i, j := range data.Objects { 133 | if i != "file" { 134 | s += j + "_" 135 | } 136 | } 137 | key = fmt.Sprintf("%v_%v%v", time.Now().Format("2006-01-02T15-04-05Z"), s, strings.ReplaceAll(data.Name, "/", "_")) 138 | } 139 | 140 | objects := map[string]string{ 141 | "file": data.Name, 142 | "bucket": parameters.Bucket, 143 | "prefix": parameters.Prefix, 144 | "key": key, 145 | } 146 | 147 | ctx := context.Background() 148 | 149 | if err := putObject(ctx, client, parameters.Bucket, parameters.Prefix, key, *data); err != nil { 150 | return utils.LogLine{ 151 | Objects: objects, 152 | Error: err.Error(), 153 | Status: utils.FailureStr, 154 | }, err 155 | } 156 | 157 | return utils.LogLine{ 158 | Objects: objects, 159 | Output: fmt.Sprintf("The file '%v' has been uploaded as the key '%v' to the bucket '%v'", data.Name, parameters.Prefix+key, parameters.Bucket), 160 | Status: utils.SuccessStr, 161 | }, nil 162 | } 163 | 164 | func putObject(ctx context.Context, storageClient client.GcpGcsAPI, bucketName, prefix, key string, data models.Data) error { 165 | bucket := storageClient.Bucket(bucketName) 166 | objectName := prefix + key 167 | wc := bucket.Object(objectName).NewWriter(ctx) 168 | defer wc.Close() 169 | 170 | if _, err := wc.Write(data.Bytes); err != nil { 171 | return err 172 | } 173 | return nil 174 | } 175 | -------------------------------------------------------------------------------- /outputs/minio/minio.go: -------------------------------------------------------------------------------- 1 | package minio 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "strings" 9 | "time" 10 | 11 | miniosdk "github.com/minio/minio-go/v7" 12 | 13 | minio "github.com/falcosecurity/falco-talon/internal/minio/client" 14 | "github.com/falcosecurity/falco-talon/internal/models" 15 | "github.com/falcosecurity/falco-talon/internal/rules" 16 | "github.com/falcosecurity/falco-talon/utils" 17 | ) 18 | 19 | const ( 20 | Name string = "s3" 21 | Category string = "minio" 22 | Description string = "Store on Minio" 23 | Permissions string = `` 24 | Example string = `- action: Get logs of the pod 25 | actionner: kubernetes:download 26 | parameters: 27 | tail_lines: 200 28 | output: 29 | target: minio:s3 30 | parameters: 31 | bucket: falco-talon 32 | prefix: /files 33 | ` 34 | ) 35 | 36 | const ( 37 | defaultContentType string = "text/plain; charset=UTF-8" 38 | ) 39 | 40 | type Parameters struct { 41 | Bucket string `mapstructure:"bucket" validate:"required"` 42 | Prefix string `mapstructure:"prefix" validate:""` 43 | } 44 | 45 | type Output struct{} 46 | 47 | func Register() *Output { 48 | return new(Output) 49 | } 50 | 51 | func (o Output) Init() error { return minio.Init() } 52 | 53 | func (o Output) Information() models.Information { 54 | return models.Information{ 55 | Name: Name, 56 | FullName: Category + ":" + Name, 57 | Category: Category, 58 | Description: Description, 59 | Permissions: Permissions, 60 | Example: Example, 61 | } 62 | } 63 | func (o Output) Parameters() models.Parameters { 64 | return Parameters{ 65 | Prefix: "", 66 | Bucket: "", 67 | } 68 | } 69 | 70 | func (o Output) Checks(output *rules.Output) error { 71 | var parameters Parameters 72 | err := utils.DecodeParams(output.GetParameters(), ¶meters) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | ctx := context.Background() 78 | exist, err := minio.GetClient().BucketExists(ctx, parameters.Bucket) 79 | if err != nil { 80 | return err 81 | } 82 | if !exist { 83 | return fmt.Errorf("the bucket '%v' does not exist", parameters.Bucket) 84 | } 85 | 86 | return nil 87 | } 88 | 89 | func (o Output) Run(output *rules.Output, data *models.Data) (utils.LogLine, error) { 90 | var parameters Parameters 91 | err := utils.DecodeParams(output.GetParameters(), ¶meters) 92 | if err != nil { 93 | return utils.LogLine{ 94 | Objects: nil, 95 | Error: err.Error(), 96 | Status: utils.FailureStr, 97 | }, err 98 | } 99 | 100 | parameters.Prefix = strings.TrimSuffix(parameters.Prefix, "/") + "/" 101 | 102 | var key string 103 | switch { 104 | case data.Objects["namespace"] != "" && data.Objects["pod"] != "": 105 | key = fmt.Sprintf("%v_%v_%v_%v", time.Now().Format("2006-01-02T15-04-05Z"), data.Objects["namespace"], data.Objects["pod"], strings.ReplaceAll(data.Name, "/", "_")) 106 | case data.Objects["hostname"] != "": 107 | key = fmt.Sprintf("%v_%v_%v", time.Now().Format("2006-01-02T15-04-05Z"), data.Objects["hostname"], strings.ReplaceAll(data.Name, "/", "_")) 108 | default: 109 | var s string 110 | for i, j := range data.Objects { 111 | if i != "file" { 112 | s += j + "_" 113 | } 114 | } 115 | key = fmt.Sprintf("%v_%v%v", time.Now().Format("2006-01-02T15-04-05Z"), s, strings.ReplaceAll(data.Name, "/", "_")) 116 | } 117 | 118 | objects := map[string]string{ 119 | "file": data.Name, 120 | "bucket": parameters.Bucket, 121 | "prefix": parameters.Prefix, 122 | "key": key, 123 | } 124 | 125 | if err := putObject(parameters.Bucket, parameters.Prefix, key, *data); err != nil { 126 | return utils.LogLine{ 127 | Objects: objects, 128 | Error: err.Error(), 129 | Status: utils.FailureStr, 130 | }, err 131 | } 132 | 133 | return utils.LogLine{ 134 | Objects: objects, 135 | Output: fmt.Sprintf("the file '%v' has been uploaded as the key '%v' to the bucket '%v'", data.Name, parameters.Prefix+key, parameters.Bucket), 136 | Status: utils.SuccessStr, 137 | }, nil 138 | } 139 | 140 | func (o Output) CheckParameters(output *rules.Output) error { 141 | var parameters Parameters 142 | 143 | err := utils.DecodeParams(output.GetParameters(), ¶meters) 144 | if err != nil { 145 | return err 146 | } 147 | 148 | err = utils.ValidateStruct(parameters) 149 | if err != nil { 150 | return err 151 | } 152 | 153 | return nil 154 | } 155 | 156 | func putObject(bucket, prefix, key string, data models.Data) error { 157 | client := minio.GetClient() 158 | if client == nil { 159 | return errors.New("client error") 160 | } 161 | 162 | ctx := context.Background() 163 | body := bytes.NewReader(data.Bytes) 164 | 165 | _, err := client.PutObject(ctx, bucket, prefix+key, body, int64(len(data.Bytes)), miniosdk.PutObjectOptions{ContentType: defaultContentType}) 166 | if err != nil { 167 | return err 168 | } 169 | return nil 170 | } 171 | -------------------------------------------------------------------------------- /outputs/outputs.go: -------------------------------------------------------------------------------- 1 | package outputs 2 | 3 | import ( 4 | "github.com/falcosecurity/falco-talon/internal/rules" 5 | awss3 "github.com/falcosecurity/falco-talon/outputs/aws/s3" 6 | "github.com/falcosecurity/falco-talon/outputs/file" 7 | "github.com/falcosecurity/falco-talon/outputs/gcs" 8 | "github.com/falcosecurity/falco-talon/outputs/minio" 9 | 10 | "github.com/falcosecurity/falco-talon/internal/models" 11 | "github.com/falcosecurity/falco-talon/utils" 12 | ) 13 | 14 | type Output interface { 15 | Init() error 16 | Run(output *rules.Output, data *models.Data) (utils.LogLine, error) 17 | CheckParameters(action *rules.Output) error 18 | Checks(output *rules.Output) error 19 | Information() models.Information 20 | Parameters() models.Parameters 21 | } 22 | 23 | type Outputs []Output 24 | 25 | var defaultOutputs *Outputs 26 | var enabledOutputs *Outputs 27 | 28 | func init() { 29 | defaultOutputs = new(Outputs) 30 | defaultOutputs = ListDefaultOutputs() 31 | enabledOutputs = new(Outputs) 32 | } 33 | 34 | func ListDefaultOutputs() *Outputs { 35 | if len(*defaultOutputs) == 0 { 36 | defaultOutputs.Add( 37 | file.Register(), 38 | minio.Register(), 39 | awss3.Register(), 40 | gcs.Register(), 41 | ) 42 | } 43 | 44 | return defaultOutputs 45 | } 46 | 47 | func (outputs *Outputs) Add(output ...Output) { 48 | for _, i := range output { 49 | *outputs = append(*outputs, i) 50 | } 51 | } 52 | 53 | func GetOutputs() *Outputs { 54 | return enabledOutputs 55 | } 56 | 57 | func (outputs *Outputs) FindOutput(fullname string) Output { 58 | if outputs == nil { 59 | return nil 60 | } 61 | 62 | for _, i := range *outputs { 63 | if i == nil { 64 | continue 65 | } 66 | if fullname == i.Information().FullName { 67 | return i 68 | } 69 | } 70 | return nil 71 | } 72 | 73 | func Init() error { 74 | rules := rules.GetRules() 75 | 76 | categories := map[string]bool{} 77 | enabledCategories := map[string]bool{} 78 | 79 | // list actionner categories to init 80 | for _, i := range *rules { 81 | for _, j := range i.Actions { 82 | if j.GetOutput() != nil { 83 | if o := ListDefaultOutputs().FindOutput(j.GetOutput().Target); o != nil { 84 | categories[o.Information().Category] = true 85 | } 86 | } 87 | } 88 | } 89 | 90 | for category := range categories { 91 | for _, output := range *defaultOutputs { 92 | if category == output.Information().Category { 93 | if err := output.Init(); err != nil { 94 | utils.PrintLog(utils.ErrorStr, utils.LogLine{Message: "init", Error: err.Error(), Category: category, Status: utils.FailureStr}) 95 | return err 96 | } 97 | enabledCategories[category] = true 98 | } 99 | } 100 | } 101 | 102 | for i := range enabledCategories { 103 | for _, j := range *defaultOutputs { 104 | if i == j.Information().Category { 105 | enabledOutputs.Add(j) 106 | } 107 | } 108 | } 109 | 110 | return nil 111 | } 112 | -------------------------------------------------------------------------------- /rules.yaml: -------------------------------------------------------------------------------- 1 | - action: Terminate Pod 2 | actionner: kubernetes:terminate 3 | parameters: 4 | grace_period_seconds: 5 5 | ignore_standalone_pods: true 6 | 7 | - action: Disable outbound connections 8 | actionner: kubernetes:networkpolicy 9 | parameters: 10 | allow: 11 | - "192.168.1.0/24" 12 | - "172.17.0.0/16" 13 | 14 | - action: Create cilium network policy 15 | actionner: cilium:networkpolicy 16 | parameters: 17 | allow_cidr: 18 | - "192.168.1.0/24" 19 | - "172.17.0.0/16" 20 | 21 | - action: Label Pod as Suspicious 22 | description: "Add the label suspicious=true" 23 | actionner: kubernetes:label 24 | parameters: 25 | labels: 26 | suspicious: "true" 27 | 28 | - action: Invoke Lambda function 29 | actionner: aws:lambda 30 | additional_contexts: 31 | - aws 32 | parameters: 33 | aws_lambda_name: sample-function 34 | aws_lambda_alias_or_version: $LATEST 35 | aws_lambda_invocation_type: RequestResponse 36 | 37 | - action: Invoke GCP function 38 | actionner: gcp:function 39 | additional_contexts: 40 | - aws 41 | parameters: 42 | gcp_function_name: simple-http-function 43 | gcp_function_location: us-central1 44 | 45 | - rule: Suspicious outbound connection 46 | description: "Label pods with suspicious outbound connections if not in the kube-system" 47 | match: 48 | rules: 49 | - Unexpected outbound connection destination 50 | output_fields: 51 | - k8s.ns.name!=kube-system 52 | actions: 53 | - action: Create cilium network policy 54 | 55 | - rule: Terminal shell in container 56 | description: > 57 | Label the pod outside kube-system and falco namespaces if a shell is started inside 58 | match: 59 | rules: 60 | - Terminal shell in container 61 | output_fields: 62 | - k8s.ns.name!=kube-system, k8s.ns.name!=falco 63 | actions: 64 | - action: Terminate Pod 65 | 66 | # - rule: Test invoke lambda 67 | # match: 68 | # rules: 69 | # - Test invoke lambda 70 | # actions: 71 | # - action: Invoke Lambda function 72 | 73 | # - rule: Test invoke GCP function 74 | # match: 75 | # rules: 76 | # - Test invoke GCP function 77 | # actions: 78 | # - action: Invoke GCP function 79 | 80 | - rule: Delete unknown namespace 81 | match: 82 | rules: 83 | - K8s Namespace Created 84 | output_fields: 85 | - ka.target.namespace=todelete 86 | actions: 87 | - action: Delete the namespace 88 | actionner: kubernetes:delete 89 | 90 | - rule: Calico netpol 91 | match: 92 | rules: 93 | - Unexpected outbound connection destination 94 | output_fields: 95 | - k8s.ns.name!=kube-system 96 | actions: 97 | - action: Disable outbound connections 98 | parameters: 99 | allow_cidr: 100 | - "192.168.1.0/24" 101 | - "172.17.0.0/16" 102 | allow_namespaces: 103 | - "green-ns" 104 | - "blue-ns" 105 | - action: Create Calico netpol 106 | actionner: calico:networkpolicy 107 | parameters: 108 | order: 20 109 | allow_cidr: 110 | - "192.168.2.0/24" 111 | allow_namespaces: 112 | - "green-ns" 113 | 114 | - rule: Test node drain 115 | match: 116 | rules: 117 | - Test node drain 118 | actions: 119 | - action: Cordon node 120 | actionner: kubernetes:cordon 121 | - action: Drain node 122 | actionner: kubernetes:drain 123 | parameters: 124 | force: true 125 | ignore_daemonsets: true 126 | ignore_statefulsets: true 127 | max_wait_period: 90 128 | 129 | - rule: Delete namespace 130 | match: 131 | rules: 132 | - Test delete namespace 133 | output_fields: 134 | - ka.target.name=todelete 135 | actions: 136 | - action: Delete the resource 137 | actionner: kubernetes:delete 138 | 139 | - rule: Test exec 140 | match: 141 | rules: 142 | - Test exec 143 | output_fields: 144 | - k8s.ns.name!=kube-system 145 | actions: 146 | - action: Test exec 147 | actionner: kubernetes:exec 148 | additional_contexts: 149 | - k8snode 150 | parameters: 151 | command: echo "${NODE_HOSTNAME}" 152 | 153 | - rule: Test download 154 | match: 155 | rules: 156 | - Test download 157 | actions: 158 | - action: Test exec 159 | actionner: kubernetes:exec 160 | additional_contexts: 161 | - k8snode 162 | parameters: 163 | shell: /bin/sh 164 | command: uname -a 165 | - action: Test log 166 | actionner: kubernetes:log 167 | output: 168 | target: aws:s3 169 | parameters: 170 | bucket: falcosidekick-tests 171 | prefix: logs/ 172 | - action: Test download 173 | actionner: kubernetes:download 174 | parameters: 175 | file: "${FD_NAME}" 176 | output: 177 | target: minio:s3 178 | parameters: 179 | bucket: falco-talon 180 | prefix: /files/ 181 | 182 | - rule: Test tcpdump 183 | match: 184 | rules: 185 | - Test tcpdump 186 | actions: 187 | - action: Test tcpdump 188 | actionner: kubernetes:tcpdump 189 | parameters: 190 | snaplen: 512 191 | duration: 5 192 | output: 193 | target: aws:s3 194 | parameters: 195 | bucket: falcosidekick-tests 196 | prefix: /tcpdump/ 197 | region: us-east-1 198 | 199 | - rule: Test sysdig 200 | match: 201 | rules: 202 | - Test sysdig 203 | actions: 204 | - action: Test sysdig 205 | actionner: kubernetes:sysdig 206 | parameters: 207 | duration: 5 208 | scope: pod 209 | buffer_size: 4096 210 | output: 211 | target: local:file 212 | parameters: 213 | destination: /tmp/ 214 | 215 | - rule: Test log 216 | match: 217 | rules: 218 | - Test log 219 | actions: 220 | - action: Test log 221 | actionner: kubernetes:log 222 | parameters: 223 | tail_lines: 1 224 | output: 225 | target: aws:s3 226 | parameters: 227 | bucket: falcosidekick-tests 228 | prefix: /logs/ 229 | region: us-east-1 -------------------------------------------------------------------------------- /rules_override.yaml: -------------------------------------------------------------------------------- 1 | - action: Terminate Pod 2 | parameters: 3 | ignore_daemonsets: true 4 | ignore_statefulsets: true 5 | grace_period_seconds: 2 --------------------------------------------------------------------------------