diff --git a/.github/workflows/latest-release.yaml b/.github/workflows/latest-release.yaml new file mode 100644 index 0000000..2bb6cc7 --- /dev/null +++ b/.github/workflows/latest-release.yaml @@ -0,0 +1,53 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2023 Authors of SentryFlow + +name: Latest release +on: + push: + branches: + - main + +permissions: read-all + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + files-changed: + name: Find out which files were changed + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + sentryflow: ${{ steps.filter.outputs.sentryflow}} + envoyfilter: ${{ steps.filter.outputs.envoyfilter}} + steps: + - uses: actions/checkout@v4 + - uses: dorny/paths-filter@v3.0.2 + id: filter + with: + filters: | + sentryflow: + - 'sentryflow/**' + envoyfilter: + - 'filter/envoy/envoy-wasm-filters/**' + + release-sentryflow-image: + needs: [ files-changed ] + if: ${{ github.repository == '5GSEC/sentryflow' && needs.files-changed.outputs.sentryflow == 'true' }} + name: Build and push sentryflow's image + uses: ./.github/workflows/release-image.yaml + with: + WORKING_DIRECTORY: ./sentryflow + NAME: sentryflow + secrets: inherit + + release-envoy-filter-image: + needs: [ files-changed ] + if: ${{ github.repository == '5GSEC/sentryflow' && needs.files-changed.outputs.envoyfilter == 'true' }} + name: Build and push envoyfilter's image + uses: ./.github/workflows/release-image.yaml + with: + WORKING_DIRECTORY: ./filter/envoy/envoy-wasm-filters + NAME: sentryflow-httpfilter + secrets: inherit diff --git a/.github/workflows/pr-checks.yaml b/.github/workflows/pr-checks.yaml index 2f340a2..548f447 100644 --- a/.github/workflows/pr-checks.yaml +++ b/.github/workflows/pr-checks.yaml @@ -14,6 +14,24 @@ on: permissions: read-all jobs: + files-changed: + name: Find out which files were changed + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + sentryflow: ${{ steps.filter.outputs.sentryflow}} + envoyfilter: ${{ steps.filter.outputs.envoyfilter}} + steps: + - uses: actions/checkout@v4 + - uses: dorny/paths-filter@v3.0.2 + id: filter + with: + filters: | + sentryflow: + - 'sentryflow/**' + envoyfilter: + - 'filter/envoy/envoy-wasm-filters/**' + license: name: License runs-on: ubuntu-latest @@ -29,6 +47,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} static-checks: + needs: [ files-changed ] + if: ${{ github.repository == '5GSEC/sentryflow' && needs.files-changed.outputs.sentryflow == 'true' }} name: Static checks runs-on: ubuntu-latest defaults: @@ -40,10 +60,10 @@ jobs: - name: Setup go uses: actions/setup-go@v5 with: - go-version: '1.22' + go-version: '1.23' - - name: go fmt - run: make fmt + - name: go fmt vet + run: make fmt vet - name: Lint uses: golangci/golangci-lint-action@v6 @@ -54,6 +74,8 @@ jobs: skip-cache: true # https://github.com/golangci/golangci-lint-action/issues/244#issuecomment-1052197778 build-sentryflow-image: + needs: [ files-changed ] + if: ${{ github.repository == '5GSEC/sentryflow' && needs.files-changed.outputs.sentryflow == 'true' }} name: Build SentryFlow container image runs-on: ubuntu-latest timeout-minutes: 20 @@ -73,3 +95,26 @@ jobs: image: "docker.io/5gsec/sentryflow:latest" severity-cutoff: critical output-format: sarif + + build-envoy-filter-image: + needs: [ files-changed ] + if: ${{ github.repository == '5GSEC/sentryflow' && needs.files-changed.outputs.envoyfilter == 'true' }} + name: Build Envoy WASM Filter container image + runs-on: ubuntu-latest + timeout-minutes: 20 + defaults: + run: + working-directory: ./filter/envoy/envoy-wasm-filters + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Build image + run: make image + + - name: Scan image + uses: anchore/scan-action@v4 + with: + image: "docker.io/5gsec/sentryflow-httpfilter:latest" + severity-cutoff: critical + output-format: sarif diff --git a/.github/workflows/release-image.yaml b/.github/workflows/release-image.yaml new file mode 100644 index 0000000..2b4d5dd --- /dev/null +++ b/.github/workflows/release-image.yaml @@ -0,0 +1,62 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2023 Authors of Nimbus + +name: Release image + +permissions: read-all + +on: + workflow_call: + inputs: + WORKING_DIRECTORY: + description: 'current working directory' + required: true + type: string + NAME: + description: 'app name' + required: true + type: string + +jobs: + release-image: + timeout-minutes: 30 + runs-on: ubuntu-latest + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Get tag + id: tag + run: | + if [ ${{ github.ref }} == "refs/heads/main" ]; then + echo "tag=latest" >> $GITHUB_OUTPUT + else + echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT + fi + + - name: Build image + run: DOCKER_TAG=${{ steps.tag.outputs.tag }} make image + working-directory: ${{ inputs.WORKING_DIRECTORY }} + + - name: Scan image + uses: anchore/scan-action@v4 + with: + image: 'docker.io/5gsec/${{ inputs.NAME }}:${{ steps.tag.outputs.tag }}' + severity-cutoff: critical + output-format: sarif + + - name: Build and push image + working-directory: ${{ inputs.WORKING_DIRECTORY }} + run: DOCKER_TAG=${{ steps.tag.outputs.tag }} make imagex diff --git a/.github/workflows/stable-release.yaml b/.github/workflows/stable-release.yaml new file mode 100644 index 0000000..2d5b964 --- /dev/null +++ b/.github/workflows/stable-release.yaml @@ -0,0 +1,54 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2024 Authors of SentryFlow + +name: Stable release + +on: + create: + tags: + - "v*" + +permissions: read-all + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + files-changed: + name: Find out which files were changed + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + sentryflow: ${{ steps.filter.outputs.sentryflow}} + envoyfilter: ${{ steps.filter.outputs.envoyfilter}} + steps: + - uses: actions/checkout@v4 + - uses: dorny/paths-filter@v3.0.2 + id: filter + with: + filters: | + sentryflow: + - 'sentryflow/**' + envoyfilter: + - 'filter/envoy/envoy-wasm-filters/**' + + release-sentryflow-image: + needs: [ files-changed ] + if: ${{ github.repository == '5GSEC/sentryflow' && needs.files-changed.outputs.sentryflow == 'true' }} + name: Build and push sentryflow image + uses: ./.github/workflows/release-image.yaml + with: + WORKING_DIRECTORY: ./sentryflow + NAME: sentryflow + secrets: inherit + + release-envoy-filter-image: + needs: [ files-changed ] + if: ${{ github.repository == '5GSEC/sentryflow' && needs.files-changed.outputs.envoyfilter == 'true' }} + name: Build and push envoyfilter's image + uses: ./.github/workflows/release-image.yaml + with: + WORKING_DIRECTORY: filter/envoy/envoy-wasm-filters + NAME: sentryflow-httpfilter + secrets: inherit diff --git a/deployments/sentryflow.yaml b/deployments/sentryflow.yaml index e3542f8..13ed851 100644 --- a/deployments/sentryflow.yaml +++ b/deployments/sentryflow.yaml @@ -65,7 +65,7 @@ data: server: port: 8081 envoy: - uri: anuragrajawat/httpfilter:v0.1 + uri: 5gsec/sentryflow-httpfilter:latest receivers: serviceMeshes: @@ -96,8 +96,8 @@ spec: serviceAccountName: sentryflow containers: - name: sentryflow - image: docker.io/5gsec/sentryflow:v0.1 - imagePullPolicy: IfNotPresent + image: docker.io/5gsec/sentryflow:latest + imagePullPolicy: Always args: - --config - /var/lib/sentryflow/config.yaml diff --git a/filter/envoy/envoy-wasm-filters/.gitignore b/filter/envoy/envoy-wasm-filters/.gitignore new file mode 100644 index 0000000..1711838 --- /dev/null +++ b/filter/envoy/envoy-wasm-filters/.gitignore @@ -0,0 +1,5 @@ +/target +**/*.rs.bk +bin/ +pkg/ +wasm-pack.log diff --git a/filter/envoy/envoy-wasm-filters/Cargo.lock b/filter/envoy/envoy-wasm-filters/Cargo.lock new file mode 100644 index 0000000..0f27087 --- /dev/null +++ b/filter/envoy/envoy-wasm-filters/Cargo.lock @@ -0,0 +1,180 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "envoy-wasm-filters" +version = "0.1.0" +dependencies = [ + "log", + "proxy-wasm", + "serde", + "serde_json", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proxy-wasm" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14a5a4df5a1ab77235e36a0a0f638687ee1586d21ee9774037693001e94d4e11" +dependencies = [ + "hashbrown", + "log", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "serde" +version = "1.0.209" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.209" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "2.0.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/filter/envoy/envoy-wasm-filters/Cargo.toml b/filter/envoy/envoy-wasm-filters/Cargo.toml new file mode 100644 index 0000000..232134b --- /dev/null +++ b/filter/envoy/envoy-wasm-filters/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "envoy-wasm-filters" +version = "0.1.0" +authors = ["Anurag Rajawat", "anuragsinghrajawat22@gmail.com"] +edition = "2021" + +[lib] +name = "httpfilters" +path = "src/lib.rs" +crate-type = ["cdylib"] + +[dependencies] +proxy-wasm = "0.2.2" +log = "0.4.22" +serde_json = "1.0.127" +serde = { version = "1.0.209", features = ["derive"] } + +[profile.release] +# Tell `rustc` to optimize for small code size. +opt-level = "s" diff --git a/filter/envoy/envoy-wasm-filters/Dockerfile b/filter/envoy/envoy-wasm-filters/Dockerfile new file mode 100644 index 0000000..f672cde --- /dev/null +++ b/filter/envoy/envoy-wasm-filters/Dockerfile @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2024 Authors of SentryFlow + +FROM rust:1.81.0 AS builder + +WORKDIR /envoy-plugin + +COPY . . + +RUN make toolchain build + +FROM scratch + +COPY --from=builder /envoy-plugin/target/wasm32-unknown-unknown/release/httpfilters.wasm ./plugin.wasm diff --git a/filter/envoy/envoy-wasm-filters/LICENSE_APACHE b/filter/envoy/envoy-wasm-filters/LICENSE_APACHE new file mode 100644 index 0000000..1b5ec8b --- /dev/null +++ b/filter/envoy/envoy-wasm-filters/LICENSE_APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/filter/envoy/envoy-wasm-filters/Makefile b/filter/envoy/envoy-wasm-filters/Makefile new file mode 100644 index 0000000..fc68e41 --- /dev/null +++ b/filter/envoy/envoy-wasm-filters/Makefile @@ -0,0 +1,38 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2024 Authors of SentryFlow + +REGISTRY ?= docker.io/5gsec +DOCKER_IMAGE ?= $(REGISTRY)/sentryflow-httpfilter +DOCKER_TAG ?= latest +CONTAINER_TOOL ?= docker + +.PHONY: help +help: ## Display this help + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +.DEFAULT_GOAL := help + +.PHONY: toolchain +toolchain: ## Install Rust WASM toolchain + @test rustup || curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + @rustup target add wasm32-unknown-unknown + +.PHONY: build +build: ## Build plugin. + @cargo build --target wasm32-unknown-unknown --release + +.PHONY: clean +clean: ## Remove generated stuff. + @cargo clean + +.PHONY: image +image: ## Build Plugin's container image + $(CONTAINER_TOOL) build -t ${DOCKER_IMAGE}:${DOCKER_TAG} . + +.PHONY: push +push: ## Push Plugin's container image + $(CONTAINER_TOOL) push ${DOCKER_IMAGE}:${DOCKER_TAG} + +.PHONY: imagex +imagex: ## Build and push Plugin's multi-platform container image. + $(CONTAINER_TOOL) buildx build --push --platform=linux/arm64,linux/amd64 -t ${DOCKER_IMAGE}:${DOCKER_TAG} . diff --git a/filter/envoy/envoy-wasm-filters/Readme.md b/filter/envoy/envoy-wasm-filters/Readme.md new file mode 100644 index 0000000..20c71b8 --- /dev/null +++ b/filter/envoy/envoy-wasm-filters/Readme.md @@ -0,0 +1,110 @@ +# Envoy Wasm HTTP Filter + +HTTP filter to observe RESTful and gRPC API calls made to/from a k8s workload. + +## Sample API Event: + +```json +{ + "metadata": { + "context_id": 3, + "timestamp": 1726211548, + "istio_version": "1.23.0", + "mesh_id": "cluster.local", + "node_name": "kind-control-plane" + }, + "request": { + "headers": { + ":scheme": "http", + ":method": "GET", + "x-envoy-decorator-operation": "filterserver.sentryflow.svc.cluster.local:80/*", + ":authority": "filterserver.sentryflow", + "user-agent": "Wget", + "x-forwarded-proto": "http", + "x-request-id": "6b2e87df-257c-931e-a996-5517b8155b4a" + }, + "body": "" + }, + "response": { + "headers": { + "date": "Fri, 13 Sep 2024 07:12:28 GMT", + "content-type": "application/json; charset=utf-8", + "content-length": "63", + ":status": "404" + }, + "body": "{\"message\":\"The specified route / not found\",\"status\":\"failed\"}" + }, + "source": { + "name": "httpd-c6d6cb94b-v259g", + "namespace": "default", + "ip": "10.244.0.27", + "port": 54636 + }, + "destination": { + "name": "", + "namespace": "sentryflow", + "ip": "10.96.158.214", + "port": 80 + }, + "protocol": "HTTP/1.1" +} +``` + +# Getting Started + +## Install development tools + +You'll need these tools for a smooth development experience: + +- [Make](https://www.gnu.org/software/make/#download), +- [Rust](https://www.rust-lang.org/tools/install) toolchain, +- An IDE ([RustRover](https://www.jetbrains.com/rust/) / [VS Code](https://code.visualstudio.com/download)), +- Container tools ([Docker](https://www.docker.com/) / [Podman](https://podman.io/)), +- [Kubernetes cluster](https://kubernetes.io/docs/setup/) running version 1.26 or later, +- [kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl) version 1.26 or later. + +## In Envoy alone + +This example can be run with docker compose and has a matching [envoy configuration](envoy.yaml) file. + +- Build the plugin + ```shell + make + ``` + +- Start the envoy container + ```shell + docker compose up + ``` + +- See the Raw API Events in `server` cluster configured in [envoy configuration](envoy.yaml). + +# In Kubernetes + +- [Install Istio](https://istio.io/latest/docs/setup/install/) +- Build the plugin + ```shell + make + ``` + +- Build and push plugin's container image + ```shell + make imagex push + ``` + +- Update the value of `filters.envoy.uri` with the latest image in + SentryFlow's [configMap](https://github.com/5GSEC/SentryFlow/blob/main/deployments/sentryflow.yaml#L68) + +- Deploy SentryFlow + ```shell + kubectl apply -f https://raw.githubusercontent.com/5GSEC/SentryFlow/refs/heads/main/deployments/sentryflow.yaml + ``` + +- Enable the envoy proxy injection by labeling the namespace in which you'll deploy workload: + ```shell + kubectl label ns istio-injection=enabled + ``` +- Deploy some workload and generate traffic by calling some APIs. For e.g., you can use + Google's [microservices-demo](https://github.com/GoogleCloudPlatform/microservices-demo). + +- Use SentryFlow's client to see the API Events. \ No newline at end of file diff --git a/filter/envoy/envoy-wasm-filters/docker-compose.yaml b/filter/envoy/envoy-wasm-filters/docker-compose.yaml new file mode 100644 index 0000000..0bc9775 --- /dev/null +++ b/filter/envoy/envoy-wasm-filters/docker-compose.yaml @@ -0,0 +1,16 @@ +services: + envoy: + image: envoyproxy/envoy:v1.31-latest + hostname: envoy + volumes: + - ./envoy.yaml:/etc/envoy/envoy.yaml + - ./target/wasm32-unknown-unknown/release:/etc/envoy/proxy-wasm-plugins + networks: + - envoymesh + ports: + - "10000:10000" + environment: + UPSTREAM: filterserver + +networks: + envoymesh: { } \ No newline at end of file diff --git a/filter/envoy/envoy-wasm-filters/envoy.yaml b/filter/envoy/envoy-wasm-filters/envoy.yaml new file mode 100644 index 0000000..76c80e7 --- /dev/null +++ b/filter/envoy/envoy-wasm-filters/envoy.yaml @@ -0,0 +1,64 @@ +static_resources: + listeners: + address: + socket_address: + address: 0.0.0.0 + port_value: 10000 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress_http + codec_type: AUTO + route_config: + name: local_routes + virtual_hosts: + - name: local_service + domains: + - "*" + routes: + - match: + prefix: "/" + direct_response: + status: 200 + body: + inline_string: "Namaste world!" + http_filters: + - name: envoy.filters.http.wasm + typed_config: + "@type": type.googleapis.com/udpa.type.v1.TypedStruct + type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm + value: + config: + name: "httpfilter" + root_id: "httpfilter" + configuration: + "@type": "type.googleapis.com/google.protobuf.StringValue" + value: | + { + "upstream_name": "filterserver", + "authority": "sentryflow", + "api_path": "/v1/api/telemetry" + } + vm_config: + runtime: "envoy.wasm.runtime.v8" + code: + local: + filename: "/etc/envoy/proxy-wasm-plugins/httpfilters.wasm" + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + clusters: + - name: "filterserver" + connect_timeout: 1s +# type: STRICT_DNS + load_assignment: + cluster_name: "filterserver" + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 192.168.31.226 # + port_value: 8080 diff --git a/filter/envoy/envoy-wasm-filters/envoyfilter.yaml b/filter/envoy/envoy-wasm-filters/envoyfilter.yaml new file mode 100644 index 0000000..59c3754 --- /dev/null +++ b/filter/envoy/envoy-wasm-filters/envoyfilter.yaml @@ -0,0 +1,29 @@ +apiVersion: networking.istio.io/v1alpha3 +kind: EnvoyFilter +metadata: + name: http-filter + namespace: istio-system +spec: + configPatches: + - applyTo: CLUSTER + match: + # Apply to all listeners/routes/clusters in both sidecars and gateways. + # https://istio.io/latest/docs/reference/config/networking/envoy-filter/#EnvoyFilter-PatchContext + context: ANY + patch: + operation: ADD + value: + connect_timeout: 1s + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: sentryflow + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: sentryflow.sentryflow + port_value: 8081 + protocol: TCP + name: sentryflow + type: LOGICAL_DNS diff --git a/filter/envoy/envoy-wasm-filters/src/lib.rs b/filter/envoy/envoy-wasm-filters/src/lib.rs new file mode 100644 index 0000000..f4df5a6 --- /dev/null +++ b/filter/envoy/envoy-wasm-filters/src/lib.rs @@ -0,0 +1,289 @@ +use log::error; +use proxy_wasm::traits::{Context, HttpContext, RootContext}; +use proxy_wasm::types::{Action, ContextType, LogLevel}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::time::{Duration, UNIX_EPOCH}; + +#[derive(Default)] +struct Plugin { + _context_id: u32, + config: PluginConfig, + api_event: APIEvent, +} + +#[derive(Deserialize, Clone, Default)] +struct PluginConfig { + upstream_name: String, + api_path: String, + authority: String, +} + +#[derive(Serialize, Default, Clone)] +struct APIEvent { + metadata: Metadata, + request: Reqquest, + response: Ressponse, + source: Workload, + destination: Workload, + protocol: String, +} + +#[derive(Serialize, Default, Clone)] +struct Metadata { + context_id: u32, + timestamp: u64, + istio_version: String, + mesh_id: String, + node_name: String, +} + +#[derive(Serialize, Default, Clone)] +struct Workload { + name: String, + namespace: String, + ip: String, + port: u16, +} + +#[derive(Serialize, Clone, Default)] +struct Reqquest { + headers: HashMap, + body: String, +} + +#[derive(Serialize, Clone, Default, Debug)] +struct Ressponse { + headers: HashMap, + body: String, +} + +const MAX_BODY_SIZE: usize = 1_000_000; // 1 MB + +fn _start() { + proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Warn); + proxy_wasm::set_root_context(|_| -> Box {Box::new(Plugin::default())}); + }} +} + +impl Context for Plugin { + fn on_done(&mut self) -> bool { + dispatch_http_call_to_upstream(self); + true + } +} + +impl RootContext for Plugin { + fn on_configure(&mut self, _plugin_configuration_size: usize) -> bool { + if let Some(config_bytes) = self.get_plugin_configuration() { + if let Ok(config) = serde_json::from_slice::(&config_bytes) { + self.config = config; + } else { + error!("Failed to parse plugin config"); + } + } else { + error!("No plugin config found"); + } + true + } + + fn create_http_context(&self, _context_id: u32) -> Option> { + Some(Box::new(Plugin { + _context_id, + config: self.config.clone(), + api_event: Default::default(), + })) + } + + fn get_type(&self) -> Option { + Some(ContextType::HttpContext) + } +} + +impl HttpContext for Plugin { + fn on_http_request_headers(&mut self, _num_headers: usize, _end_of_stream: bool) -> Action { + let (src_ip, src_port) = get_url_and_port( + String::from_utf8( + self.get_property(vec!["source", "address"]) + .unwrap_or_default(), + ) + .unwrap_or_default(), + ); + + let req_headers = self.get_http_request_headers(); + let mut headers: HashMap = HashMap::with_capacity(req_headers.len()); + for header in req_headers { + // Don't include Envoy's pseudo headers + // https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers#id13 + if !header.0.starts_with("x-envoy") { + headers.insert(header.0, header.1); + } + } + + self.api_event.metadata.timestamp = self + .get_current_time() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + self.api_event.metadata.context_id = self._context_id; + self.api_event.request.headers = headers; + + let protocol = String::from_utf8( + self.get_property(vec!["request", "protocol"]) + .unwrap_or_default(), + ) + .unwrap_or_default(); + self.api_event.protocol = protocol; + + self.api_event.source.ip = src_ip; + self.api_event.source.port = src_port; + self.api_event.source.name = String::from_utf8( + self.get_property(vec!["node", "metadata", "NAME"]) + .unwrap_or_default(), + ) + .unwrap_or_default(); + self.api_event.source.namespace = String::from_utf8( + self.get_property(vec!["node", "metadata", "NAMESPACE"]) + .unwrap_or_default(), + ) + .unwrap_or_default(); + + Action::Continue + } + + fn on_http_request_body(&mut self, _body_size: usize, _end_of_stream: bool) -> Action { + let body = String::from_utf8( + self.get_http_request_body(0, _body_size) + .unwrap_or_default(), + ) + .unwrap_or_default(); + + if !body.is_empty() && body.len() <= MAX_BODY_SIZE { + self.api_event.request.body = body; + } + Action::Continue + } + + fn on_http_response_headers(&mut self, _num_headers: usize, _end_of_stream: bool) -> Action { + let (dest_ip, dest_port) = get_url_and_port( + String::from_utf8( + self.get_property(vec!["destination", "address"]) + .unwrap_or_default(), + ) + .unwrap_or_default(), + ); + + let res_headers = self.get_http_response_headers(); + let mut headers: HashMap = HashMap::with_capacity(res_headers.len()); + for res_header in res_headers { + // Don't include Envoy's pseudo headers + // https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers#id13 + if !res_header.0.starts_with("x-envoy") { + headers.insert(res_header.0, res_header.1); + } + } + + self.api_event.response.headers = headers; + self.api_event.destination.ip = dest_ip; + self.api_event.destination.port = dest_port; + find_and_update_dest_namespace(self); + + Action::Continue + } + + fn on_http_response_body(&mut self, _body_size: usize, _end_of_stream: bool) -> Action { + let body = String::from_utf8( + self.get_http_response_body(0, _body_size) + .unwrap_or_default(), + ) + .unwrap_or_default(); + if !body.is_empty() && body.len() <= MAX_BODY_SIZE { + self.api_event.response.body = body; + } + Action::Continue + } +} + +fn find_and_update_dest_namespace(obj: &mut Plugin) { + let dest_ns = String::from_utf8( + obj.get_property(vec![ + "upstream_host_metadata", + "filter_metadata", + "istio", + "workload", + ]) + .unwrap_or_default(), + ) + .unwrap_or_default(); + + // e.g., filterserver;sentryflow;filterserver;;Kubernetes + if !dest_ns.is_empty() { + let parts: Vec<&str> = dest_ns.split(";").collect(); + if parts.len() == 5 || parts.len() == 4 { + obj.api_event.destination.namespace = parts[1].to_string(); + } + } +} + +fn dispatch_http_call_to_upstream(obj: &mut Plugin) { + update_metadata(obj); + let telemetry_json = serde_json::to_string(&obj.api_event).unwrap_or_default(); + + let headers = vec![ + (":method", "POST"), + (":authority", &obj.config.authority), + (":path", &obj.config.api_path), + ("accept", "*/*"), + ("Content-Type", "application/json"), + ]; + + let http_call_res = obj.dispatch_http_call( + &obj.config.upstream_name, + headers, + Some(telemetry_json.as_bytes()), + vec![], + Duration::from_secs(1), + ); + + if http_call_res.is_err() { + error!( + "Failed to dispatch HTTP call, to '{}' status: {http_call_res:#?}", + &obj.config.upstream_name, + ); + } +} + +fn update_metadata(obj: &mut Plugin) { + obj.api_event.metadata.node_name = String::from_utf8( + obj.get_property(vec!["node", "metadata", "NODE_NAME"]) + .unwrap_or_default(), + ) + .unwrap_or_default(); + obj.api_event.metadata.mesh_id = String::from_utf8( + obj.get_property(vec!["node", "metadata", "MESH_ID"]) + .unwrap_or_default(), + ) + .unwrap_or_default(); + obj.api_event.metadata.istio_version = String::from_utf8( + obj.get_property(vec!["node", "metadata", "ISTIO_VERSION"]) + .unwrap_or_default(), + ) + .unwrap_or_default(); +} + +fn get_url_and_port(address: String) -> (String, u16) { + let parts: Vec<&str> = address.split(':').collect(); + + let mut url = "".to_string(); + let mut port = 0; + + if parts.len() == 2 { + url = parts[0].parse().unwrap(); + port = parts[1].parse().unwrap(); + } else { + error!("Invalid address"); + } + + (url, port) +} diff --git a/filter/envoy/envoy-wasm-filters/wasm-plugin.yaml b/filter/envoy/envoy-wasm-filters/wasm-plugin.yaml new file mode 100644 index 0000000..d73789a --- /dev/null +++ b/filter/envoy/envoy-wasm-filters/wasm-plugin.yaml @@ -0,0 +1,19 @@ +apiVersion: extensions.istio.io/v1alpha1 +kind: WasmPlugin +metadata: + name: http-filter + namespace: istio-system +spec: + # Do not cause all requests to fail with 5xx. + # Bypass the plugin execution on plugin issues. + # https://istio.io/latest/docs/reference/config/proxy_extensions/wasm-plugin/#FailStrategy + failStrategy: FAIL_OPEN + match: + - mode: CLIENT + pluginConfig: + api_path: /api/v1/events + authority: sentryflow + upstream_name: sentryflow + pluginName: http-filter + type: HTTP + url: anuragrajawat/httpfilter:v0.2 # Change this to your image while trying locally diff --git a/sentryflow/config/default.yaml b/sentryflow/config/default.yaml index 51c7907..3be12b6 100644 --- a/sentryflow/config/default.yaml +++ b/sentryflow/config/default.yaml @@ -6,7 +6,7 @@ filters: port: 8081 envoy: - uri: anuragrajawat/httpfilter:v0.1 + uri: 5gsec/sentryflow-httpfilter:latest receivers: # aka sources serviceMeshes: diff --git a/sentryflow/pkg/receiver/svcmesh/istio/sidecar/sidecar.go b/sentryflow/pkg/receiver/svcmesh/istio/sidecar/sidecar.go index 4a576b9..ba7268c 100644 --- a/sentryflow/pkg/receiver/svcmesh/istio/sidecar/sidecar.go +++ b/sentryflow/pkg/receiver/svcmesh/istio/sidecar/sidecar.go @@ -178,7 +178,7 @@ spec: configPatches: - applyTo: CLUSTER match: - context: SIDECAR_OUTBOUND + context: ANY patch: operation: ADD value: