diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 00000000..2fccaad8 --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: gomod + directory: / + schedule: + interval: daily + - package-ecosystem: github-actions + directory: / + schedule: + interval: daily diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index dfec104c..55f0985b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -4,7 +4,7 @@ on: push: branches: - main - - development + - 3.x paths-ignore: - README.md @@ -14,25 +14,62 @@ on: pull_request: branches: - main + - 3.x jobs: coverage: runs-on: ubuntu-latest steps: - - name: Set up Go 1.22 - uses: actions/setup-go@v2 + - name: Checkout + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + + - name: Set up Go + uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.2.0 with: - go-version: 1.22 - - name: Checkout code - uses: actions/checkout@v2 - - name: Get dependencies - run: go get -v -t -d ./... + go-version-file: go.mod + cache-dependency-path: go.sum + + - name: Check go.mod + run: | + set -e + go mod tidy && git diff --exit-code + + - name: Check code format + run: | + set -e + make fmt + git diff --exit-code + + - name: Run Trivy vulnerability scanner in repo mode + uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8 # v0.24.0 + with: + scan-type: 'fs' + ignore-unfixed: true + format: 'sarif' + output: 'trivy-results.sarif' + severity: 'CRITICAL,HIGH' + - name: Calc coverage run: make coverage + - name: Convert coverage to lcov - uses: jandelgado/gcov2lcov-action@v1.0.9 + uses: jandelgado/gcov2lcov-action@c680c0f7c7442485f1749eb2a13e54a686e76eb5 #v1.0.9 + - name: Coveralls - uses: coverallsapp/github-action@v2.0.0 + uses: coverallsapp/github-action@643bc377ffa44ace6394b2b5d0d3950076de9f63 # v2.3.0 with: github-token: ${{ secrets.github_token }} - file: coverage.lcov \ No newline at end of file + file: coverage.lcov + + check-actions: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - name: Ensure SHA pinned actions + uses: zgosalvez/github-actions-ensure-sha-pinned-actions@b8f9a25a51fe633d9215ac7734854dc11cd299cb # v3.0.13 + with: + # slsa-github-generator requires using a semver tag for reusable workflows. + # See: https://github.com/slsa-framework/slsa-github-generator#referencing-slsa-builders-and-generators + allowlist: | + slsa-framework/slsa-github-generator \ No newline at end of file diff --git a/.github/workflows/cr.yaml b/.github/workflows/cr.yaml deleted file mode 100644 index 129ad600..00000000 --- a/.github/workflows/cr.yaml +++ /dev/null @@ -1,29 +0,0 @@ -name: Release Charts - -on: - push: - branches: - - main - paths: - - 'charts/**' - - 'manifests/**' - -jobs: - release: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Configure Git - run: | - git config user.name "$GITHUB_ACTOR" - git config user.email "$GITHUB_ACTOR@users.noreply.github.com" - - - name: Install Helm - uses: azure/setup-helm@v1 - - - name: Run chart-releaser - uses: helm/chart-releaser-action@v1.2.1 - env: - CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" \ No newline at end of file diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 18853b2f..912f3c8d 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -10,10 +10,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@master + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Setup node env - uses: actions/setup-node@v2.1.2 + uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 #v4.0.4 with: node-version: 16 @@ -32,9 +32,8 @@ jobs: cp index.yaml ./dist/index.yaml cp artifacthub-repo.yml ./dist/artifacthub-repo.yml - - name: Deploy - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e #v4.0.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./dist diff --git a/.github/workflows/helm-chart.yaml b/.github/workflows/helm-chart.yaml index b5f4fa3b..9c74a826 100644 --- a/.github/workflows/helm-chart.yaml +++ b/.github/workflows/helm-chart.yaml @@ -5,7 +5,6 @@ on: # run pipeline on push on master branches: - main - - development paths: - "charts/**" @@ -18,16 +17,19 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: fetch-depth: "0" - name: chart-testing (ct lint) - uses: helm/chart-testing-action@v2.0.1 + uses: helm/chart-testing-action@e6669bcd63d7cb57cb4380c33043eebe5d111992 # v2.6.1 - name: Run Helm Chart lint run: | - ct lint --lint-conf=.github/ct_lintconf.yaml \ - --chart-yaml-schema=.github/ct_chart_schema.yaml \ - --target-branch=main --validate-maintainers=false \ - --chart-dirs charts + set -e + ct lint --lint-conf=.github/ct_lintconf.yaml \ + --chart-yaml-schema=.github/ct_chart_schema.yaml \ + --target-branch=main \ + --validate-maintainers=false \ + --check-version-increment=false \ + --chart-dirs charts diff --git a/.github/workflows/release-chart.yaml b/.github/workflows/release-chart.yaml new file mode 100644 index 00000000..1326640e --- /dev/null +++ b/.github/workflows/release-chart.yaml @@ -0,0 +1,65 @@ +name: release-chart + +on: + push: + tags: + - 'policy-reporter-chart-v*' + +jobs: + helm-chart: + runs-on: ubuntu-latest + permissions: + contents: write + pages: write + steps: + - name: Checkout + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + with: + fetch-depth: 0 + + - name: Verify Helm Docs + run: | + set -e + make verify-helm-docs + + - name: Configure Git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + + - name: Install Helm + uses: azure/setup-helm@fe7b79cd5ee1e45176fcad797de68ecaf3ca4814 # v4.2.0 + with: + version: v3.16.1 + + - name: Prepare GPG key + run: | + gpg_dir=.cr-gpg + mkdir "$gpg_dir" + keyring="$gpg_dir/secring.gpg" + base64 -d <<< "$GPG_KEYRING_BASE64" > "$keyring" + passphrase_file="$gpg_dir/passphrase" + echo "$GPG_PASSPHRASE" > "$passphrase_file" + echo "CR_PASSPHRASE_FILE=$passphrase_file" >> "$GITHUB_ENV" + echo "CR_KEYRING=$keyring" >> "$GITHUB_ENV" + env: + GPG_KEYRING_BASE64: "${{ secrets.GPG_KEYRING_BASE64 }}" #Referring secrets of github above + GPG_PASSPHRASE: "${{ secrets.GPG_PASSPHRASE }}" + + - name: Run chart-releaser + uses: helm/chart-releaser-action@a917fd15b20e8b64b94d9158ad54cd6345335584 # v1.6.0 + id: cr + env: + CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + CR_KEY: "${{ secrets.CR_KEY }}" + CR_SIGN: true + + - name: Install Cosign + uses: sigstore/cosign-installer@1fc5bd396d372bee37d608f955b336615edf79c8 # v3.2.0 + + - name: Push to OCI + run: | + set -e + output=$(helm push .cr-release-packages/policy-reporter-{{ steps.cr.outputs.chart_version }}.tgz oci://ghcr.io/kyverno/charts 2>&1) + digest=$( echo "$output" | grep Digest | cut -c9-) + cosign sign --yes ghcr.io/kyverno/charts/policy-reporter@$digest \ No newline at end of file diff --git a/.github/workflows/image.yaml b/.github/workflows/release-image.yaml similarity index 67% rename from .github/workflows/image.yaml rename to .github/workflows/release-image.yaml index 1dee0dfe..c99d1cc3 100644 --- a/.github/workflows/image.yaml +++ b/.github/workflows/release-image.yaml @@ -1,9 +1,16 @@ -name: image +name: release-image on: push: + branches: + - main + tags: - - v* - - dev + - 'v*' + + paths-ignore: + - README.md + - charts/** + - manifest/** permissions: contents: read @@ -15,58 +22,58 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Push image id: params run: | # Strip git ref prefix from version - VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') + VERSION=$(git rev-parse --short "$GITHUB_SHA") # Strip "v" prefix from tag name [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') echo VERSION=$VERSION - echo "::set-output name=version::$VERSION" + echo "VERSION=$VERSION" >> "$GITHUB_ENV" - name: Login to Github Packages - uses: docker/login-action@v2 + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.CR_PAT }} - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 #v3.7.1 id: buildx with: install: true version: latest - name: Build image and push to GitHub Container Registry - uses: docker/build-push-action@v3 + uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0 id: push with: push: true - platforms: linux/arm64,linux/amd64,linux/s390x + platforms: linux/arm64,linux/amd64 cache-from: type=registry,ref=ghcr.io/kyverno/policy-reporter:buildcache cache-to: type=registry,ref=ghcr.io/kyverno/policy-reporter:buildcache,mode=max tags: | - ghcr.io/kyverno/policy-reporter:latest - ghcr.io/kyverno/policy-reporter:${{ steps.params.outputs.version }} + ghcr.io/kyverno/policy-reporter:${{ env.VERSION }} - - name: Set up Go 1.22 - uses: actions/setup-go@v2 + - name: Set up Go + uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.2.0 with: - go-version: 1.22 + go-version-file: go.mod + cache-dependency-path: go.sum - uses: CycloneDX/gh-gomod-generate-sbom@efc74245d6802c8cefd925620515442756c70d8f # v2.0.0 with: version: v1 args: app -licenses -json -output policy-reporter-bom.cdx.json -main . - - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: policy-reporter-bom-cdx path: policy-reporter-bom.cdx.json diff --git a/.gitignore b/.gitignore index a7fb23a6..2d92d65d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ build /test.yaml *.db values*.yaml +monitoring.yaml coverage.out* heap* /.env* diff --git a/CHANGELOG.md b/CHANGELOG.md index 94fda6ae..76cb34f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -309,7 +309,7 @@ * Policy Reporter * New `certificate` config for `loki`, `elasticsearch`, `teams`, `webhook` and `ui`, to set the path to your custom certificate for the related client. * New `skipTLS` config for `loki`, `elasticsearch`, `teams`, `webhook` and `ui`, to skip tls if needed for the given target. - * New `secretRef` for targets to reference a secret with the related `username`, `password`, `webhook`, `host`, `accessKeyID`, `secretAccessKey` information of the given target, instead of configure your credentials directly. + * New `secretRef` for targets to reference a secret with the related `username`, `password`, `webhook`, `host`, `accessKeyId`, `secretAccessKey` information of the given target, instead of configure your credentials directly. * Policy Reporter UI * New value `refreshInterval` to configure the default refresh interval for API polling. Set `0` to disable polling. * Policy Reporter Kyverno Plugin diff --git a/DEMO.md b/DEMO.md new file mode 100644 index 00000000..bb1a3ffc --- /dev/null +++ b/DEMO.md @@ -0,0 +1,144 @@ +# Demo Instructions + +## Kind Cluster + +```bash +make kind-create-cluster +``` + +## Kyverno + +### Add Repository + +```bash +helm repo add kyverno https://kyverno.github.io/kyverno +``` + +### Install + +```bash +helm upgrade --install kyverno kyverno/kyverno -n kyverno --create-namespace +helm upgrade --install kyverno-policies kyverno/kyverno-policies --set podSecurityStandard=restricted +``` + +## Falco + +### Add Repository + +```bash +helm repo add falcosecurity https://falcosecurity.github.io/charts +``` + +### Install + +```bash +helm upgrade --install falco falcosecurity/falco --set falcosidekick.enabled=true --set falcosidekick.config.policyreport.enabled=true --set falcosidekick.image.tag=latest --namespace falco --create-namespace +``` + +## Trivy Operator + +### Add Repository + +```bash +helm repo add aqua https://aquasecurity.github.io/helm-charts/ +helm repo add trivy-operator-polr-adapter https://fjogeleit.github.io/trivy-operator-polr-adapter +``` + +### Install + +```bash +helm upgrade --install trivy-operator aqua/trivy-operator -n trivy-system --create-namespace --set="trivy.ignoreUnfixed=true" +helm upgrade --install trivy-operator-polr-adapter trivy-operator-polr-adapter/trivy-operator-polr-adapter -n trivy-system +``` + +## Policy Reporter + +### Add Repository + +```bash +helm repo add policy-reporter https://kyverno.github.io/policy-reporter +``` + +### Install + +#### Slack Secret + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: webhook-secret + namespace: policy-reporter +type: Opaque +data: + webhook: aHR0cHM6Ly9ob29rcy5z... +``` + +#### Values + +```yaml +plugin: + kyverno: + enabled: true + + trivy: + enabled: true + +ui: + enabled: true + + ingress: + enabled: true + annotations: + nginx.ingress.kubernetes.io/rewrite-target: /$1 + className: nginx + hosts: + - host: localhost + paths: + - path: "/ui/(.*)" + pathType: ImplementationSpecific + + sources: + - name: Trivy ConfigAudit + type: severity + excludes: + results: + - pass + - error + + - name: Trivy Vulnerability + type: severity + excludes: + results: + - pass + - error + + - name: Falco + excludes: + results: + - pass + - skip + +target: + slack: + name: Kyverno Channel + channel: kyverno + secretRef: webhook-secret + minimumSeverity: warning + skipExistingOnStartup: true + sources: [kyverno] + filter: + namespaces: + exclude: ['trivy-system'] + channels: + - name: Trivy Operator + channel: trivy-operator + sources: [Trivy Vulnerability] + filter: + namespaces: + exclude: ['trivy-system'] +``` + +```bash +helm upgrade --install policy-reporter policy-reporter/policy-reporter --create-namespace -n policy-reporter -f values.yaml --devel +``` diff --git a/Dockerfile b/Dockerfile index 4e36b9a6..b600bedc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.22 as builder +FROM golang:1.23 AS builder ARG LD_FLAGS='-s -w -linkmode external -extldflags "-static"' ARG TARGETPLATFORM diff --git a/Makefile b/Makefile index f31b1cb9..773e76d0 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,142 @@ -GO ?= go -BUILD ?= build -REPO ?= ghcr.io/kyverno/policy-reporter -IMAGE_TAG ?= 2.20.1 -LD_FLAGS=-s -w -linkmode external -extldflags "-static" -PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x +############ +# DEFAULTS # +############ + +KIND_IMAGE ?= kindest/node:v1.30.2 +KIND_NAME ?= kyverno +USE_CONFIG ?= standard,no-ingress,in-cluster,all-read-rbac +KUBECONFIG ?= "" +PIP ?= "pip3" +GO ?= go +BUILD ?= build +IMAGE_TAG ?= 3.0.0 + +############# +# VARIABLES # +############# + +GIT_SHA := $(shell git rev-parse HEAD) +TIMESTAMP := $(shell date '+%Y-%m-%d_%I:%M:%S%p') +GOOS ?= $(shell go env GOOS) +GOARCH ?= $(shell go env GOARCH) +REGISTRY ?= ghcr.io +OWNER ?= kyverno +KO_REGISTRY := ko.local +IMAGE ?= policy-reporter +LD_FLAGS := -s -w -linkmode external -extldflags "-static" +LOCAL_PLATFORM := linux/$(GOARCH) +PLATFORMS := linux/arm64,linux/amd64,linux/s390x +REPO := $(REGISTRY)/$(OWNER)/$(IMAGE) +COMMA := , + +ifndef VERSION +APP_VERSION := $(GIT_SHA) +else +APP_VERSION := $(VERSION) +endif + +######### +# TOOLS # +######### + +TOOLS_DIR := $(PWD)/.tools +KIND := $(TOOLS_DIR)/kind +KIND_VERSION := v0.24.0 +KO := $(TOOLS_DIR)/ko +KO_VERSION := v0.15.1 +HELM := $(TOOLS_DIR)/helm +HELM_VERSION := v3.10.1 +HELM_DOCS := $(TOOLS_DIR)/helm-docs +HELM_DOCS_VERSION := v1.11.0 +GCI := $(TOOLS_DIR)/gci +GCI_VERSION := v0.9.1 +GOFUMPT := $(TOOLS_DIR)/gofumpt +GOFUMPT_VERSION := v0.4.0 +TOOLS := $(HELM) $(HELM_DOCS) $(GCI) $(GOFUMPT) + +$(HELM): + @echo Install helm... >&2 + @GOBIN=$(TOOLS_DIR) go install helm.sh/helm/v3/cmd/helm@$(HELM_VERSION) + +$(HELM_DOCS): + @echo Install helm-docs... >&2 + @GOBIN=$(TOOLS_DIR) go install github.com/norwoodj/helm-docs/cmd/helm-docs@$(HELM_DOCS_VERSION) + +$(GCI): + @echo Install gci... >&2 + @GOBIN=$(TOOLS_DIR) go install github.com/daixiang0/gci@$(GCI_VERSION) + +$(GOFUMPT): + @echo Install gofumpt... >&2 + @GOBIN=$(TOOLS_DIR) go install mvdan.cc/gofumpt@$(GOFUMPT_VERSION) + +$(KIND): + @echo Install kind... >&2 + @GOBIN=$(TOOLS_DIR) go install sigs.k8s.io/kind@$(KIND_VERSION) + +$(KO): + @echo Install ko... >&2 + @GOBIN=$(TOOLS_DIR) go install github.com/google/ko@$(KO_VERSION) + +.PHONY: gci +gci: $(GCI) + @echo "Running gci" + @$(GCI) write -s standard -s default -s "prefix(github.com/kyverno/policy-reporter)" . + +.PHONY: gofumpt +gofumpt: $(GOFUMPT) + @echo "Running gofumpt" + @$(GOFUMPT) -w . + +.PHONY: fmt +fmt: gci gofumpt + +.PHONY: install-tools +install-tools: $(TOOLS) ## Install tools + +.PHONY: clean-tools +clean-tools: ## Remove installed tools + @echo Clean tools... >&2 + @rm -rf $(TOOLS_DIR) + +######## +# KIND # +######## + +.PHONY: kind-create-cluster +kind-create-cluster: $(KIND) ## Create kind cluster + @echo Create kind cluster... >&2 + @$(KIND) create cluster --name $(KIND_NAME) --image $(KIND_IMAGE) --config ./scripts/kind.yaml + @kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml + @sleep 15 + @kubectl wait --namespace ingress-nginx --for=condition=ready pod --selector=app.kubernetes.io/component=controller --timeout=90s + +.PHONY: kind-delete-cluster +kind-delete-cluster: $(KIND) ## Delete kind cluster + @echo Delete kind cluster... >&2 + @$(KIND) delete cluster --name $(KIND_NAME) + +.PHONY: kind-load +kind-load: $(KIND) ko-build ## Build playground image and load it in kind cluster + @echo Load playground image... >&2 + @$(KIND) load docker-image --name $(KIND_NAME) ko.local/github.com/kyverno/policy-reporter:$(GIT_SHA) + +########### +# CODEGEN # +########### + +.PHONY: codegen-helm-docs +codegen-helm-docs: ## Generate helm docs + @echo Generate helm docs... >&2 + @docker run -v ${PWD}/charts:/work -w /work jnorwood/helm-docs:v1.11.0 -s file + +.PHONY: verify-helm-docs +verify-helm-docs: codegen-helm-docs ## Check Helm charts are up to date + @echo Checking helm charts are up to date... >&2 + @git --no-pager diff -- charts + @echo 'If this test fails, it is because the git diff is non-empty after running "make codegen-helm-docs".' >&2 + @echo 'To correct this, locally run "make codegen-helm-docs", commit the changes, and re-run tests.' >&2 + @git diff --quiet --exit-code -- charts all: build @@ -41,11 +174,3 @@ docker-push: .PHONY: docker-push-dev docker-push-dev: @docker buildx build --progress plane --platform $(PLATFORMS) --tag $(REPO):dev . --build-arg LD_FLAGS='$(LD_FLAGS) -X main.Version=$(IMAGE_TAG)-dev' --push - -.PHONY: fmt -fmt: - $(call print-target) - @echo "Running gci" - @go run github.com/daixiang0/gci@v0.9.1 write -s standard -s default -s "prefix(github.com/kyverno/policy-reporter)" . - @echo "Running gofumpt" - @go run mvdan.cc/gofumpt@v0.4.0 -w . diff --git a/README.md b/README.md index 896ce4fe..af4b3e37 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,20 @@ -# Policy Reporter +# Policy Reporter 3.x Preview [![CI](https://github.com/kyverno/policy-reporter/actions/workflows/ci.yaml/badge.svg)](https://github.com/kyverno/policy-reporter/actions/workflows/ci.yaml) [![Go Report Card](https://goreportcard.com/badge/github.com/kyverno/policy-reporter)](https://goreportcard.com/report/github.com/kyverno/policy-reporter) [![Coverage Status](https://coveralls.io/repos/github/kyverno/policy-reporter/badge.svg?branch=main)](https://coveralls.io/github/kyverno/policy-reporter?branch=main) -## Motivation -Kyverno ships with two types of validation. You can either enforce a rule or audit it. If you don't want to block developers or if you want to try out a new rule, you can use the audit functionality. The audit configuration creates [PolicyReports](https://kyverno.io/docs/policy-reports/) which you can access with `kubectl`. Because I can't find a simple solution to get a general overview of this PolicyReports and PolicyReportResults, I created this tool to send information about PolicyReports to different targets like [Grafana Loki](https://grafana.com/oss/loki/), [Elasticsearch](https://www.elastic.co/de/elasticsearch/) or [Slack](https://slack.com/). +![Screenshot Policy Reporter UI v2](https://github.com/kyverno/policy-reporter/blob/3.x/docs/images/screen.png) -Policy Reporter provides also a Prometheus Metrics API as well as an standalone mode along with the [Policy Reporter UI](https://kyverno.github.io/policy-reporter/guide/getting-started#core--policy-reporter-ui). - -This project is in an early stage. Please let me know if anything did not work as expected or if you want to send your audits to unsupported targets. ## Documentation -You can find detailed Information and Screens about Features and Configurations in the [Documentation](https://kyverno.github.io/policy-reporter). +Documentation for upcoming features and changes for the new Policy Reporter UI v2 are located in [Docs](https://github.com/kyverno/policy-reporter/tree/3.x/docs) + +* [Installation](https://github.com/kyverno/policy-reporter/blob/3.x/docs/SETUP.md) +* [OAUth2 / OpenIDConnect](https://github.com/kyverno/policy-reporter/blob/3.x/docs/UI_AUTH.md) +* [UI CustomBoards](https://github.com/kyverno/policy-reporter/blob/3.x/docs/CUSTOM_BOARDS.md) +* [Kyverno PolicyExceptions](https://github.com/kyverno/policy-reporter/blob/3.x/docs/EXCEPTIONS.md) + +The new documentation page for Policy Reporter v3 can be found here: [https://kyverno.github.io/policy-reporter-docs/](https://kyverno.github.io/policy-reporter-docs/) ## Getting Started @@ -25,60 +28,10 @@ helm repo add policy-reporter https://kyverno.github.io/policy-reporter helm repo update ``` -### Basic Installation - -The basic installation provides optional Prometheus Metrics and/or optional REST APIs, for more details have a look at the [Documentation](https://kyverno.github.io/policy-reporter/guide/getting-started). - -```bash -helm install policy-reporter policy-reporter/policy-reporter -n policy-reporter --set metrics.enabled=true --set rest.enabled=true --create-namespace -``` - -### Installation without Helm or Kustomize - -To install Policy Reporter without Helm or Kustomize have a look at [manifests](https://github.com/kyverno/policy-reporter/tree/main/manifest). - -## Policy Reporter UI - -You can use the Policy Reporter as standalone Application along with the optional UI SubChart. - ### Installation with Policy Reporter UI and Kyverno Plugin enabled ```bash -helm install policy-reporter policy-reporter/policy-reporter --set kyvernoPlugin.enabled=true --set ui.enabled=true --set ui.plugins.kyverno=true -n policy-reporter --create-namespace +helm install policy-reporter policy-reporter/policy-reporter --create-namespace -n policy-reporter --devel --set ui.enabled=true --set kyverno-plugin.enabled=true kubectl port-forward service/policy-reporter-ui 8082:8080 -n policy-reporter ``` -Open `http://localhost:8082/` in your browser. -Check the [Documentation](https://kyverno.github.io/policy-reporter/guide/getting-started#core--policy-reporter-ui) for Screens and additional Information - -## Targets - -Policy Reporter supports the following [Targets](https://kyverno.github.io/policy-reporter/core/targets) to send new (Cluster)PolicyReport Results too: -* [Grafana Loki](https://kyverno.github.io/policy-reporter/core/targets#grafana-loki) -* [Elasticsearch](https://kyverno.github.io/policy-reporter/core/targets#elasticsearch) -* [Microsoft Teams](https://kyverno.github.io/policy-reporter/core/targets#microsoft-teams) -* [Slack](https://kyverno.github.io/policy-reporter/core/targets#slack) -* [Discord](https://kyverno.github.io/policy-reporter/core/targets#discord) -* [Policy Reporter UI](https://kyverno.github.io/policy-reporter/core/targets#policy-reporter-ui) -* [Webhook](https://kyverno.github.io/policy-reporter/core/targets#webhook) -* [S3](https://kyverno.github.io/policy-reporter/core/targets#s3-compatible-storage) -* [AWS Kinesis compatible Services](https://kyverno.github.io/policy-reporter/core/targets#kinesis-compatible-services) -* [AWS SecurityHub](https://kyverno.github.io/policy-reporter/core/targets#aws-securityhub) -* [Google Cloud Storage](https://kyverno.github.io/policy-reporter/core/targets/#google-cloud-storage) -* [Telegram](https://kyverno.github.io/policy-reporter/core/targets#telegram) -* [Google Chat](https://kyverno.github.io/policy-reporter/core/targets#google-chat) - -## Monitoring - -The Helm Chart includes optional SubChart for [Prometheus Operator](https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack) Integration. The provided Dashboards working without Loki and are only based on the Prometheus Metrics. - -Have a look into the [Documentation](https://kyverno.github.io/policy-reporter/guide/helm-chart-core/#configure-the-servicemonitor) for details. - -### Grafana Dashboard Import - -If you are not using the MonitoringStack you can import the dashboards from [Grafana](https://grafana.com/orgs/policyreporter/dashboards) - -## Resources - -* [[Video] 37. #EveryoneCanContribute cafe: Policy reporter for Kyverno](https://youtu.be/1mKywg9f5Fw) -* [[Video] Rawkode Live: Hands on Policy Reporter](https://www.youtube.com/watch?v=ZrOtTELNLyg) -* [[Blog] Monitor Security and Best Practices with Kyverno and Policy Reporter](https://blog.webdev-jogeleit.de/blog/monitor-security-with-kyverno-and-policy-reporter/) +Open `http://localhost:8082/` in your browser. \ No newline at end of file diff --git a/charts/policy-reporter/Chart.lock b/charts/policy-reporter/Chart.lock deleted file mode 100644 index 640b55e4..00000000 --- a/charts/policy-reporter/Chart.lock +++ /dev/null @@ -1,12 +0,0 @@ -dependencies: -- name: monitoring - repository: "" - version: 2.8.2 -- name: ui - repository: "" - version: 2.10.5 -- name: kyvernoPlugin - repository: "" - version: 1.6.5 -digest: sha256:5ee2b291bc447466442a8ea81f94fc852352ac8ae15045525778fdea3769c7c2 -generated: "2024-02-04T10:42:39.448841+01:00" diff --git a/charts/policy-reporter/Chart.yaml b/charts/policy-reporter/Chart.yaml index 631df010..761c5065 100644 --- a/charts/policy-reporter/Chart.yaml +++ b/charts/policy-reporter/Chart.yaml @@ -5,8 +5,8 @@ description: | It creates Prometheus Metrics and can send rule validation events to different targets like Loki, Elasticsearch, Slack or Discord type: application -version: 2.24.2 -appVersion: 2.20.2 +version: 3.0.0-rc.1 +appVersion: 3.0.0-rc.1 icon: https://github.com/kyverno/kyverno/raw/main/img/logo.png home: https://kyverno.github.io/policy-reporter @@ -14,14 +14,3 @@ sources: - https://github.com/kyverno/policy-reporter maintainers: - name: Frank Jogeleit - -dependencies: - - name: monitoring - condition: monitoring.enabled - version: "2.8.2" - - name: ui - condition: ui.enabled - version: "2.10.5" - - name: kyvernoPlugin - condition: kyvernoPlugin.enabled - version: "1.6.5" diff --git a/charts/policy-reporter/README.md b/charts/policy-reporter/README.md index bc423490..a48b408c 100644 --- a/charts/policy-reporter/README.md +++ b/charts/policy-reporter/README.md @@ -1,16 +1,13 @@ -# Policy Reporter +# policy-reporter -![Version: v2.24.1](https://img.shields.io/badge/Version-v2.24.1-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v2.20.1](https://img.shields.io/badge/AppVersion-v2.20.1-informational?style=flat-square) +Policy Reporter watches for PolicyReport Resources. +It creates Prometheus Metrics and can send rule validation events to different targets like Loki, Elasticsearch, Slack or Discord -## Motivation - -Kyverno ships with two types of validation. You can either enforce a rule or audit it. If you don't want to block developers or if you want to try out a new rule, you can use the audit functionality. The audit configuration creates [PolicyReports](https://kyverno.io/docs/policy-reports/) which you can access with `kubectl`. Because I can't find a simple solution to get a general overview of this PolicyReports and PolicyReportResults, I created this tool to send information about PolicyReports to different targets like [Grafana Loki](https://grafana.com/oss/loki/), [Elasticsearch](https://www.elastic.co/de/elasticsearch/) or [Slack](https://slack.com/). +![Version: 3.0.0-rc.1](https://img.shields.io/badge/Version-3.0.0--rc.1-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 3.0.0-rc.1](https://img.shields.io/badge/AppVersion-3.0.0--rc.1-informational?style=flat-square) ## Documentation -You can find detailed Information and Screens about Features and Configurations in the [Documentation](https://kyverno.github.io/policy-reporter/guide/02-getting-started#core--policy-reporter-ui). - -## Getting Started +You can find detailed Information and Screens about Features and Configurations in the [Documentation](https://kyverno.github.io/policy-reporter-docs). ## Installation with Helm v3 @@ -35,16 +32,549 @@ helm install policy-reporter policy-reporter/policy-reporter -n policy-reporter You can use the Policy Reporter as standalone Application along with the optional UI SubChart. ### Installation with Policy Reporter UI and Kyverno Plugin enabled + ```bash -helm install policy-reporter policy-reporter/policy-reporter --set kyvernoPlugin.enabled=true --set ui.enabled=true --set ui.plugins.kyverno=true -n policy-reporter --create-namespace +helm install policy-reporter policy-reporter/policy-reporter --set plugin.kyverno.enabled=true --set ui.enabled=true -n policy-reporter --create-namespace kubectl port-forward service/policy-reporter-ui 8082:8080 -n policy-reporter ``` Open `http://localhost:8082/` in your browser. -Check the [Documentation](https://kyverno.github.io/policy-reporter/guide/02-getting-started#core--policy-reporter-ui) for Screens and additional Information +## Values -## Resources +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| nameOverride | string | `""` | Override the chart name used for all resources | +| fullnameOverride | string | `"policy-reporter"` | Overwrite the fullname of all resources | +| namespaceOverride | string | `""` | Overwrite the namespace of all resources | +| image.registry | string | `"ghcr.io"` | Image registry | +| image.repository | string | `"kyverno/policy-reporter"` | Image repository | +| image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy | +| image.tag | string | `"12da466"` | Image tag | +| imagePullSecrets | list | `[]` | Image pullSecrets | +| priorityClassName | string | `""` | Deployment priorityClassName | +| replicaCount | int | `1` | Deployment replica count | +| revisionHistoryLimit | int | `10` | The number of revisions to keep | +| updateStrategy | object | `{}` | Deployment strategy | +| port | object | `{"name":"http","number":8080}` | Container port | +| annotations | object | `{}` | Key/value pairs that are attached to all resources. | +| rbac.enabled | bool | `true` | Create RBAC resources | +| serviceAccount.create | bool | `true` | Create ServiceAccount | +| serviceAccount.automount | bool | `true` | Enable ServiceAccount automaount | +| serviceAccount.annotations | object | `{}` | Annotations for the ServiceAccount | +| serviceAccount.name | string | `""` | The ServiceAccount name | +| service.enabled | bool | `true` | Create Service | +| service.type | string | `"ClusterIP"` | Service type | +| service.port | int | `8080` | Service port | +| service.annotations | object | `{}` | Service annotations | +| service.labels | object | `{}` | Service labels | +| podSecurityContext | object | `{"fsGroup":1234}` | Security context for the pod | +| securityContext.runAsUser | int | `1234` | | +| securityContext.runAsNonRoot | bool | `true` | | +| securityContext.privileged | bool | `false` | | +| securityContext.allowPrivilegeEscalation | bool | `false` | | +| securityContext.readOnlyRootFilesystem | bool | `true` | | +| securityContext.capabilities.drop[0] | string | `"ALL"` | | +| securityContext.seccompProfile.type | string | `"RuntimeDefault"` | | +| securityContext.podAnnotations | object | `{}` | Additional annotations to add to each pod | +| securityContext.podLabels | object | `{}` | Additional labels to add to each pod | +| resources | object | `{}` | Resource constraints | +| networkPolicy.enabled | bool | `false` | Create NetworkPolicy | +| networkPolicy.egress | list | `[{"ports":[{"port":6443,"protocol":"TCP"}],"to":null}]` | Egress rule to allowe Kubernetes API Server access | +| networkPolicy.ingress | list | `[]` | | +| ingress.enabled | bool | `false` | Create Ingress This ingress exposes the policy-reporter core app. | +| ingress.className | string | `""` | Ingress className | +| ingress.labels | object | `{}` | Labels for the Ingress | +| ingress.annotations | object | `{}` | Annotations for the Ingress | +| ingress.hosts | string | `nil` | Ingress host list | +| ingress.tls | list | `[]` | Ingress tls list | +| logging.server | bool | `false` | Enables server access logging | +| logging.encoding | string | `"console"` | Log encoding possible encodings are console and json | +| logging.logLevel | int | `0` | Log level default info | +| rest.enabled | bool | `false` | Enables the REST API | +| metrics.enabled | bool | `false` | Enables Prometheus Metrics | +| metrics.mode | string | `"detailed"` | Metric Mode allowes to customize labels Allowed values: detailed, simple, custom | +| metrics.customLabels | list | `[]` | List of used labels in custom mode Supported fields are: ["namespace", "rule", "policy", "report" // PolicyReport name, "kind" // resource kind, "name" // resource name, "status", "severity", "category", "source"] | +| metrics.filter | object | `{}` | Filter results to reduce cardinality | +| profiling.enabled | bool | `false` | Enable profiling with pprof | +| worker | int | `5` | Amount of queue workers for PolicyReport resource processing | +| reportFilter | object | `{}` | Filter PolicyReport resources to process | +| sourceConfig | list | `[]` | Customize source specific logic like result ID generation | +| sourceFilters[0].selector.source | string | `"kyverno"` | select PolicyReport by source | +| sourceFilters[0].uncontrolledOnly | bool | `true` | Filter out PolicyReports of controlled Pods and Jobs, only works for PolicyReport with scope resource | +| sourceFilters[0].disableClusterReports | bool | `false` | Filter out ClusterPolicyReports | +| sourceFilters[0].kinds | object | `{"exclude":["ReplicaSet"]}` | Filter out PolicyReports based on the scope resource kind | +| global.labels | object | `{}` | additional labels added on each resource | +| basicAuth.username | string | `""` | HTTP BasicAuth username | +| basicAuth.password | string | `""` | HTTP BasicAuth password | +| basicAuth.secretRef | optional | `""` | Secret reference to get username and/or password from | +| emailReports.clusterName | optional | `""` | - Displayed in the email report if configured | +| emailReports.titlePrefix | string | `"Report"` | Title prefix in the email subject | +| emailReports.resources | object | `{}` | Resource constraints for the created CronJobs | +| emailReports.smtp.secret | optional | `""` | Secret reference to provide the complete or partial SMTP configuration | +| emailReports.smtp.host | string | `""` | SMTP Server Host | +| emailReports.smtp.port | int | `465` | SMTP Server Port | +| emailReports.smtp.username | string | `""` | SMTP Username | +| emailReports.smtp.password | string | `""` | SMTP Password | +| emailReports.smtp.from | string | `""` | Displayed from email address | +| emailReports.smtp.encryption | string | `""` | SMTP Encryption Default is none, supports ssl/tls and starttls | +| emailReports.smtp.skipTLS | bool | `false` | Skip SMTP TLS verification | +| emailReports.smtp.certificate | string | `""` | SMTP Server Certificate file path | +| emailReports.summary.enabled | bool | `false` | Enable Summary E-Mail reports | +| emailReports.summary.schedule | string | `"0 8 * * *"` | CronJob schedule | +| emailReports.summary.activeDeadlineSeconds | int | `300` | CronJob activeDeadlineSeconds | +| emailReports.summary.backoffLimit | int | `3` | CronJob backoffLimit | +| emailReports.summary.ttlSecondsAfterFinished | int | `0` | CronJob ttlSecondsAfterFinished | +| emailReports.summary.restartPolicy | string | `"Never"` | CronJob restartPolicy | +| emailReports.summary.to | list | `[]` | List of receiver email addresses | +| emailReports.summary.filter | optional | `{}` | Report filter | +| emailReports.summary.channels | optional | `[]` | Channels can be used to to send only a subset of namespaces / sources to dedicated email addresses | +| emailReports.violations.enabled | bool | `false` | Enable Violation Summary E-Mail reports | +| emailReports.violations.schedule | string | `"0 8 * * *"` | CronJob schedule | +| emailReports.violations.activeDeadlineSeconds | int | `300` | CronJob activeDeadlineSeconds | +| emailReports.violations.backoffLimit | int | `3` | CronJob backoffLimit | +| emailReports.violations.ttlSecondsAfterFinished | int | `0` | CronJob ttlSecondsAfterFinished | +| emailReports.violations.restartPolicy | string | `"Never"` | CronJob restartPolicy | +| emailReports.violations.to | list | `[]` | List of receiver email addresses | +| emailReports.violations.filter | optional | `{}` | Report filter | +| emailReports.violations.channels | optional | `[]` | Channels can be used to to send only a subset of namespaces / sources to dedicated email addresses | +| existingTargetConfig.enabled | bool | `false` | Use an already existing configuration | +| existingTargetConfig.name | string | `""` | Name of the secret with the config | +| existingTargetConfig.subPath | string | `""` | SubPath within the secret (defaults to config.yaml) | +| target.loki.host | string | `""` | Host Address | +| target.loki.path | string | `""` | Loki API, defaults to "/loki/api/v1/push" | +| target.loki.certificate | string | `""` | Server Certificate file path Can be added under extraVolumes | +| target.loki.skipTLS | bool | `false` | Skip TLS verification | +| target.loki.secretRef | string | `""` | Read configuration from an already existing Secret | +| target.loki.mountedSecret | string | `""` | Mounted secret path by Secrets Controller, secret should be in json format | +| target.loki.minimumSeverity | string | `""` | Minimum severity: "" < info < low < medium < high < critical | +| target.loki.sources | list | `[]` | List of sources which should send | +| target.loki.skipExistingOnStartup | bool | `true` | Skip already existing PolicyReportResults on startup | +| target.loki.customFields | object | `{}` | Added as additional labels | +| target.loki.headers | object | `{}` | Additional HTTP Headers | +| target.loki.username | string | `""` | HTTP BasicAuth username | +| target.loki.password | string | `""` | HTTP BasicAuth password | +| target.loki.filter | object | `{}` | Filter Results which should send to this target Wildcars for namespaces and policies are supported, you can either define exclude or include values Filters are available for all targets except the UI | +| target.loki.channels | list | `[]` | List of channels to route results to different configurations | +| target.elasticsearch.host | string | `""` | Host address | +| target.elasticsearch.certificate | string | `""` | Server Certificate file path Can be added under extraVolumes | +| target.elasticsearch.skipTLS | bool | `false` | Skip TLS verification | +| target.elasticsearch.index | string | `"policy-reporter"` | Elasticsearch index (default: policy-reporter) | +| target.elasticsearch.rotation | string | `"daily"` | Elasticsearch index rotation and index suffix Possible values: daily, monthly, annually, none (default: daily) | +| target.elasticsearch.typelessApi | bool | `false` | Enables Elasticsearch typless API https://www.elastic.co/blog/moving-from-types-to-typeless-apis-in-elasticsearch-7-0 keeping as false for retrocompatibility. | +| target.elasticsearch.username | string | `""` | HTTP BasicAuth username | +| target.elasticsearch.password | string | `""` | HTTP BasicAuth password | +| target.elasticsearch.apiKey | string | `""` | Elasticsearch API Key for api key authentication | +| target.elasticsearch.secretRef | string | `""` | Read configuration from an already existing Secret | +| target.elasticsearch.mountedSecret | string | `""` | Mounted secret path by Secrets Controller, secret should be in json format | +| target.elasticsearch.minimumSeverity | string | `""` | Minimum severity: "" < info < low < medium < high < critical | +| target.elasticsearch.sources | list | `[]` | List of sources which should send | +| target.elasticsearch.skipExistingOnStartup | bool | `true` | Skip already existing PolicyReportResults on startup | +| target.elasticsearch.customFields | object | `{}` | Added as additional labels | +| target.elasticsearch.filter | object | `{}` | Filter Results which should send to this target Wildcars for namespaces and policies are supported, you can either define exclude or include values Filters are available for all targets except the UI | +| target.elasticsearch.channels | list | `[]` | List of channels to route results to different configurations | +| target.slack.webhook | string | `""` | Webhook Address | +| target.slack.channel | string | `""` | Slack Channel | +| target.slack.secretRef | string | `""` | Read configuration from an already existing Secret | +| target.slack.mountedSecret | string | `""` | Mounted secret path by Secrets Controller, secret should be in json format | +| target.slack.minimumSeverity | string | `""` | Minimum severity: "" < info < low < medium < high < critical | +| target.slack.sources | list | `[]` | List of sources which should send | +| target.slack.skipExistingOnStartup | bool | `true` | Skip already existing PolicyReportResults on startup | +| target.slack.customFields | object | `{}` | Added as additional labels | +| target.slack.filter | object | `{}` | Filter Results which should send to this target Wildcars for namespaces and policies are supported, you can either define exclude or include values Filters are available for all targets except the UI | +| target.slack.channels | list | `[]` | List of channels to route results to different configurations | +| target.discord.webhook | string | `""` | Webhook Address | +| target.discord.secretRef | string | `""` | Read configuration from an already existing Secret | +| target.discord.mountedSecret | string | `""` | Mounted secret path by Secrets Controller, secret should be in json format | +| target.discord.minimumSeverity | string | `""` | Minimum severity: "" < info < low < medium < high < critical | +| target.discord.sources | list | `[]` | List of sources which should send | +| target.discord.skipExistingOnStartup | bool | `true` | Skip already existing PolicyReportResults on startup | +| target.discord.customFields | object | `{}` | Added as additional labels | +| target.discord.filter | object | `{}` | Filter Results which should send to this target Wildcars for namespaces and policies are supported, you can either define exclude or include values Filters are available for all targets except the UI | +| target.discord.channels | list | `[]` | List of channels to route results to different configurations | +| target.teams.webhook | string | `""` | Webhook Address | +| target.teams.secretRef | string | `""` | Read configuration from an already existing Secret | +| target.teams.mountedSecret | string | `""` | Mounted secret path by Secrets Controller, secret should be in json format | +| target.teams.minimumSeverity | string | `""` | Minimum severity: "" < info < low < medium < high < critical | +| target.teams.sources | list | `[]` | List of sources which should send | +| target.teams.skipExistingOnStartup | bool | `true` | Skip already existing PolicyReportResults on startup | +| target.teams.customFields | object | `{}` | Added as additional labels | +| target.teams.filter | object | `{}` | Filter Results which should send to this target Wildcars for namespaces and policies are supported, you can either define exclude or include values Filters are available for all targets except the UI | +| target.teams.channels | list | `[]` | List of channels to route results to different configurations | +| target.webhook.host | string | `""` | Webhook Address | +| target.webhook.headers | object | `{}` | Additional HTTP Headers | +| target.webhook.secretRef | string | `""` | Read configuration from an already existing Secret | +| target.webhook.mountedSecret | string | `""` | Mounted secret path by Secrets Controller, secret should be in json format | +| target.webhook.minimumSeverity | string | `""` | Minimum severity: "" < info < low < medium < high < critical | +| target.webhook.sources | list | `[]` | List of sources which should send | +| target.webhook.skipExistingOnStartup | bool | `true` | Skip already existing PolicyReportResults on startup | +| target.webhook.customFields | object | `{}` | Added as additional labels | +| target.webhook.filter | object | `{}` | Filter Results which should send to this target Wildcars for namespaces and policies are supported, you can either define exclude or include values Filters are available for all targets except the UI | +| target.webhook.channels | list | `[]` | List of channels to route results to different configurations | +| target.telegram.token | string | `""` | Telegram bot token | +| target.telegram.chatId | string | `""` | Telegram chat id | +| target.telegram.host | optional | `""` | Telegram proxy host | +| target.telegram.headers | object | `{}` | Additional HTTP Headers | +| target.telegram.secretRef | string | `""` | Read configuration from an already existing Secret | +| target.telegram.mountedSecret | string | `""` | Mounted secret path by Secrets Controller, secret should be in json format | +| target.telegram.minimumSeverity | string | `""` | Minimum severity: "" < info < low < medium < high < critical | +| target.telegram.sources | list | `[]` | List of sources which should send | +| target.telegram.skipExistingOnStartup | bool | `true` | Skip already existing PolicyReportResults on startup | +| target.telegram.customFields | object | `{}` | Added as additional labels | +| target.telegram.filter | object | `{}` | Filter Results which should send to this target Wildcars for namespaces and policies are supported, you can either define exclude or include values Filters are available for all targets except the UI | +| target.telegram.channels | list | `[]` | List of channels to route results to different configurations | +| target.googleChat.webhook | string | `""` | Webhook Address | +| target.googleChat.headers | object | `{}` | Additional HTTP Headers | +| target.googleChat.secretRef | string | `""` | Read configuration from an already existing Secret | +| target.googleChat.mountedSecret | string | `""` | Mounted secret path by Secrets Controller, secret should be in json format | +| target.googleChat.minimumSeverity | string | `""` | Minimum severity: "" < info < low < medium < high < critical | +| target.googleChat.sources | list | `[]` | List of sources which should send | +| target.googleChat.skipExistingOnStartup | bool | `true` | Skip already existing PolicyReportResults on startup | +| target.googleChat.customFields | object | `{}` | Added as additional labels | +| target.googleChat.filter | object | `{}` | Filter Results which should send to this target Wildcars for namespaces and policies are supported, you can either define exclude or include values Filters are available for all targets except the UI | +| target.googleChat.channels | list | `[]` | List of channels to route results to different configurations | +| target.s3.accessKeyId | optional | `""` | S3 Access key | +| target.s3.secretAccessKey | optional | `""` | S3 SecretAccess key | +| target.s3.region | optional | `""` | S3 Storage region | +| target.s3.endpoint | optional | `""` | S3 Storage endpoint | +| target.s3.bucket | required | `""` | S3 Storage bucket name | +| target.s3.bucketKeyEnabled | bool | `false` | S3 Storage to use an S3 Bucket Key for object encryption with SSE-KMS | +| target.s3.kmsKeyId | string | `""` | S3 Storage KMS Key ID for object encryption with SSE-KMS | +| target.s3.serverSideEncryption | string | `""` | S3 Storage server-side encryption algorithm used when storing this object in Amazon S3, AES256, aws:kms | +| target.s3.pathStyle | bool | `false` | S3 Storage, force path style configuration | +| target.s3.prefix | string | `""` | Used prefix, keys will have format: s3:////YYYY-MM-DD/YYYY-MM-DDTHH:mm:ss.s+01:00.json | +| target.s3.secretRef | string | `""` | Read configuration from an already existing Secret | +| target.s3.mountedSecret | string | `""` | Mounted secret path by Secrets Controller, secret should be in json format | +| target.s3.minimumSeverity | string | `""` | Minimum severity: "" < info < low < medium < high < critical | +| target.s3.sources | list | `[]` | List of sources which should send | +| target.s3.skipExistingOnStartup | bool | `true` | Skip already existing PolicyReportResults on startup | +| target.s3.customFields | object | `{}` | Added as additional labels | +| target.s3.filter | object | `{}` | Filter Results which should send to this target Wildcars for namespaces and policies are supported, you can either define exclude or include values Filters are available for all targets except the UI | +| target.s3.channels | list | `[]` | List of channels to route results to different configurations | +| target.kinesis.accessKeyId | optional | `""` | Access key | +| target.kinesis.secretAccessKey | optional | `""` | SecretAccess key | +| target.kinesis.region | optional | `""` | Region | +| target.kinesis.endpoint | optional | `""` | Endpoint | +| target.kinesis.streamName | required | `""` | StreamName | +| target.kinesis.secretRef | string | `""` | Read configuration from an already existing Secret | +| target.kinesis.mountedSecret | string | `""` | Mounted secret path by Secrets Controller, secret should be in json format | +| target.kinesis.minimumSeverity | string | `""` | Minimum severity: "" < info < low < medium < high < critical | +| target.kinesis.sources | list | `[]` | List of sources which should send | +| target.kinesis.skipExistingOnStartup | bool | `true` | Skip already existing PolicyReportResults on startup | +| target.kinesis.customFields | object | `{}` | Added as additional labels | +| target.kinesis.filter | object | `{}` | Filter Results which should send to this target Wildcars for namespaces and policies are supported, you can either define exclude or include values Filters are available for all targets except the UI | +| target.kinesis.channels | list | `[]` | List of channels to route results to different configurations | +| target.securityHub.accessKeyId | optional | `""` | Access key | +| target.securityHub.secretAccessKey | optional | `""` | SecretAccess key | +| target.securityHub.region | optional | `""` | Region | +| target.securityHub.endpoint | optional | `""` | Endpoint | +| target.securityHub.accountId | required | `""` | AccountId | +| target.securityHub.productName | optional | `""` | Used product name, defaults to "Polilcy Reporter" | +| target.securityHub.companyName | optional | `""` | Used company name, defaults to "Kyverno" | +| target.securityHub.synchronize | bool | `true` | Enable cleanup listener for SecurityHub | +| target.securityHub.delayInSeconds | int | `2` | Delay between AWS GetFindings API calls, to avoid hitting the API RequestLimit | +| target.securityHub.secretRef | string | `""` | Read configuration from an already existing Secret | +| target.securityHub.mountedSecret | string | `""` | Mounted secret path by Secrets Controller, secret should be in json format | +| target.securityHub.minimumSeverity | string | `""` | Minimum severity: "" < info < low < medium < high < critical | +| target.securityHub.sources | list | `[]` | List of sources which should send | +| target.securityHub.skipExistingOnStartup | bool | `true` | Skip already existing PolicyReportResults on startup | +| target.securityHub.customFields | object | `{}` | Added as additional labels | +| target.securityHub.filter | object | `{}` | Filter Results which should send to this target Wildcars for namespaces and policies are supported, you can either define exclude or include values Filters are available for all targets except the UI | +| target.securityHub.channels | list | `[]` | List of channels to route results to different configurations | +| target.gcs.credentials | optional | `""` | GCS (Google Cloud Storage) Service Accout Credentials | +| target.gcs.bucket | required | `""` | GCS Bucket | +| target.gcs.secretRef | string | `""` | Read configuration from an already existing Secret | +| target.gcs.mountedSecret | string | `""` | Mounted secret path by Secrets Controller, secret should be in json format | +| target.gcs.minimumSeverity | string | `""` | Minimum severity: "" < info < low < medium < high < critical | +| target.gcs.sources | list | `[]` | List of sources which should send | +| target.gcs.skipExistingOnStartup | bool | `true` | Skip already existing PolicyReportResults on startup | +| target.gcs.customFields | object | `{}` | Added as additional labels | +| target.gcs.filter | object | `{}` | Filter Results which should send to this target Wildcars for namespaces and policies are supported, you can either define exclude or include values Filters are available for all targets except the UI | +| target.gcs.channels | list | `[]` | List of channels to route results to different configurations | +| leaderElection.releaseOnCancel | bool | `true` | | +| leaderElection.leaseDuration | int | `15` | | +| leaderElection.renewDeadline | int | `10` | | +| leaderElection.retryPeriod | int | `2` | | +| redis.enabled | bool | `false` | Enables Redis as external result cache, uses in memory cache by default | +| redis.address | string | `""` | Redis host | +| redis.database | int | `0` | Redis database | +| redis.prefix | string | `"policy-reporter"` | Redis key prefix | +| redis.username | optional | `""` | Username | +| redis.password | optional | `""` | Password | +| database.type | string | `""` | Use an external Database, supported: mysql, postgres, mariadb | +| database.database | string | `""` | Database | +| database.username | string | `""` | Username | +| database.password | string | `""` | Password | +| database.host | string | `""` | Host Address | +| database.enableSSL | bool | `false` | Enables SSL | +| database.dsn | string | `""` | Instead of configure the individual values you can also provide an DSN string example postgres: postgres://postgres:password@localhost:5432/postgres?sslmode=disable example mysql: root:password@tcp(localhost:3306)/test?tls=false | +| database.secretRef | string | `""` | Read configuration from an existing Secret supported fields: username, password, host, dsn, database | +| database.mountedSecret | string | `""` | | +| podDisruptionBudget.minAvailable | int | `1` | Configures the minimum available pods for policy-reporter disruptions. Cannot be used if `maxUnavailable` is set. | +| podDisruptionBudget.maxUnavailable | string | `nil` | Configures the maximum unavailable pods for policy-reporter disruptions. Cannot be used if `minAvailable` is set. | +| nodeSelector | object | `{}` | Node labels for pod assignment ref: https://kubernetes.io/docs/user-guide/node-selection/ | +| tolerations | list | `[]` | Tolerations for pod assignment ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ | +| affinity | object | `{}` | Anti-affinity to disallow deploying client and master nodes on the same worker node | +| topologySpreadConstraints | list | `[]` | Topology Spread Constraints to better spread pods | +| livenessProbe | object | `{"httpGet":{"path":"/ready","port":"http"}}` | Deployment livenessProbe for policy-reporter | +| readinessProbe | object | `{"httpGet":{"path":"/healthz","port":"http"}}` | Deployment readinessProbe for policy-reporter | +| extraVolumes.volumeMounts | list | `[]` | Deployment volumeMounts | +| extraVolumes.volumes | list | `[]` | Deployment values | +| sqliteVolume | object | `{}` | If set the volume for sqlite is freely configurable below "- name: sqlite". If no value is set an emptyDir is used. | +| envVars | list | `[]` | Allow additional env variables to be added | +| tmpVolume | object | `{}` | Allow custom configuration of the /tmp volume | +| ui.enabled | bool | `false` | Enable Policy Reporter UI | +| ui.image.registry | string | `"ghcr.io"` | Image registry | +| ui.image.repository | string | `"kyverno/policy-reporter-ui"` | Image repository | +| ui.image.pullPolicy | string | `"IfNotPresent"` | Image PullPolicy | +| ui.image.tag | string | `"2.0.0-rc.1"` | Image tag | +| ui.replicaCount | int | `1` | Deployment replica count | +| ui.tempDir | string | `"/tmp"` | Temporary Directory to persist session data for authentication | +| ui.logging.api | bool | `false` | Enables external api request logging | +| ui.logging.server | bool | `false` | Enables server access logging | +| ui.logging.encoding | string | `"console"` | Log encoding possible encodings are console and json | +| ui.logging.logLevel | int | `0` | Log level default info | +| ui.server.port | int | `8080` | Application port | +| ui.server.cors | bool | `true` | Enabled CORS header | +| ui.server.overwriteHost | bool | `true` | Overwrites Request Host with Proxy Host and adds `X-Forwarded-Host` and `X-Origin-Host` headers | +| ui.openIDConnect.enabled | bool | `false` | Enable openID Connect authentication | +| ui.openIDConnect.discoveryUrl | string | `""` | OpenID Connect Discovery URL | +| ui.openIDConnect.callbackUrl | string | `""` | OpenID Connect Callback URL | +| ui.openIDConnect.clientId | string | `""` | OpenID Connect ClientID | +| ui.openIDConnect.clientSecret | string | `""` | OpenID Connect ClientSecret | +| ui.openIDConnect.scopes | list | `[]` | OpenID Connect allowed Scopes | +| ui.openIDConnect.secretRef | string | `""` | Provide OpenID Connect configuration via Secret supported keys: `discoveryUrl`, `clientId`, `clientSecret` | +| ui.oauth.enabled | bool | `false` | Enable openID Connect authentication | +| ui.oauth.provider | string | `""` | OAuth2 Provider supported: amazon, gitlab, github, apple, google, yandex, azuread | +| ui.oauth.callbackUrl | string | `""` | OpenID Connect Callback URL | +| ui.oauth.clientId | string | `""` | OpenID Connect ClientID | +| ui.oauth.clientSecret | string | `""` | OpenID Connect ClientSecret | +| ui.oauth.scopes | list | `[]` | OpenID Connect allowed Scopes | +| ui.oauth.secretRef | string | `""` | Provide OpenID Connect configuration via Secret supported keys: `provider`, `clientId`, `clientSecret` | +| ui.banner | string | `""` | optional banner text | +| ui.displayMode | string | `""` | DisplayMode dark/light/colorblind/colorblinddark uses the OS configured prefered color scheme as default | +| ui.customBoards | list | `[]` | Additional customizable dashboards | +| ui.sources | list | `[]` | source specific configurations | +| ui.name | string | `"Default"` | | +| ui.clusters | list | `[]` | Connected Policy Reporter APIs | +| ui.imagePullSecrets | list | `[]` | Image pull secrets for image verification policies, this will define the `--imagePullSecrets` argument | +| ui.serviceAccount.create | bool | `true` | Create ServiceAccount | +| ui.serviceAccount.automount | bool | `true` | Enable ServiceAccount automaount | +| ui.serviceAccount.annotations | object | `{}` | Annotations for the ServiceAccount | +| ui.serviceAccount.name | string | `""` | The ServiceAccount name | +| ui.extraManifests | list | `[]` | list of extra manifests | +| ui.sidecarContainers | object | `{}` | Add sidecar containers to the UI deployment sidecarContainers: oauth-proxy: image: quay.io/oauth2-proxy/oauth2-proxy:v7.6.0 args: - --upstream=http://127.0.0.1:8080 - --http-address=0.0.0.0:8081 - ... ports: - containerPort: 8081 name: oauth-proxy protocol: TCP resources: {} | +| ui.podAnnotations | object | `{}` | Additional annotations to add to each pod | +| ui.podLabels | object | `{}` | Additional labels to add to each pod | +| ui.updateStrategy | object | `{}` | Deployment update strategy. Ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy | +| ui.revisionHistoryLimit | int | `10` | The number of revisions to keep | +| ui.podSecurityContext | object | `{"runAsGroup":1234,"runAsUser":1234}` | Security context for the pod | +| ui.envVars | list | `[]` | Allow additional env variables to be added | +| ui.rbac.enabled | bool | `true` | Create RBAC resources | +| ui.securityContext.runAsUser | int | `1234` | | +| ui.securityContext.runAsNonRoot | bool | `true` | | +| ui.securityContext.privileged | bool | `false` | | +| ui.securityContext.allowPrivilegeEscalation | bool | `false` | | +| ui.securityContext.readOnlyRootFilesystem | bool | `true` | | +| ui.securityContext.capabilities.drop[0] | string | `"ALL"` | | +| ui.securityContext.seccompProfile.type | string | `"RuntimeDefault"` | | +| ui.service.type | string | `"ClusterIP"` | Service type. | +| ui.service.port | int | `8080` | Service port. | +| ui.service.annotations | object | `{}` | Service annotations. | +| ui.service.labels | object | `{}` | Service labels. | +| ui.service.additionalPorts | list | `[]` | Additional service ports for e.g. Sidecars # - name: authenticated additionalPorts: - name: authenticated port: 8081 targetPort: 8081 | +| ui.ingress.enabled | bool | `false` | Create ingress resource. | +| ui.ingress.port | string | `nil` | Redirect ingress to an additional defined port on the service | +| ui.ingress.className | string | `""` | Ingress class name. | +| ui.ingress.labels | object | `{}` | Ingress labels. | +| ui.ingress.annotations | object | `{}` | Ingress annotations. | +| ui.ingress.hosts | list | `[]` | List of ingress host configurations. | +| ui.ingress.tls | list | `[]` | List of ingress TLS configurations. | +| ui.networkPolicy.enabled | bool | `false` | When true, use a NetworkPolicy to allow ingress to the webhook This is useful on clusters using Calico and/or native k8s network policies in a default-deny setup. | +| ui.networkPolicy.egress | list | `[{"ports":[{"port":6443,"protocol":"TCP"}]}]` | A list of valid from selectors according to https://kubernetes.io/docs/concepts/services-networking/network-policies. Enables Kubernetes API Server by default | +| ui.networkPolicy.ingress | list | `[]` | A list of valid from selectors according to https://kubernetes.io/docs/concepts/services-networking/network-policies. | +| ui.resources | object | `{}` | Resource constraints | +| ui.podDisruptionBudget.minAvailable | int | `1` | Configures the minimum available pods for kyvernoPlugin disruptions. Cannot be used if `maxUnavailable` is set. | +| ui.podDisruptionBudget.maxUnavailable | string | `nil` | Configures the maximum unavailable pods for kyvernoPlugin disruptions. Cannot be used if `minAvailable` is set. | +| ui.nodeSelector | object | `{}` | Node labels for pod assignment | +| ui.tolerations | list | `[]` | List of node taints to tolerate | +| ui.affinity | object | `{}` | Affinity constraints. | +| plugin.kyverno.enabled | bool | `false` | Enable Kyverno Plugin | +| plugin.kyverno.image.registry | string | `"ghcr.io"` | Image registry | +| plugin.kyverno.image.repository | string | `"kyverno/policy-reporter/kyverno-plugin"` | Image repository | +| plugin.kyverno.image.pullPolicy | string | `"IfNotPresent"` | Image PullPolicy | +| plugin.kyverno.image.tag | string | `"0.3.0"` | Image tag Defaults to `Chart.AppVersion` if omitted | +| plugin.kyverno.replicaCount | int | `1` | Deployment replica count | +| plugin.kyverno.logging.api | bool | `false` | Enables external API request logging | +| plugin.kyverno.logging.server | bool | `false` | Enables Server access logging | +| plugin.kyverno.logging.encoding | string | `"console"` | log encoding possible encodings are console and json | +| plugin.kyverno.logging.logLevel | int | `0` | log level default info | +| plugin.kyverno.server.port | int | `8080` | Application port | +| plugin.kyverno.blockReports.enabled | bool | `false` | Enables he BlockReport feature | +| plugin.kyverno.blockReports.eventNamespace | string | `"default"` | Watches for Kyverno Events in the configured namespace leave blank to watch in all namespaces | +| plugin.kyverno.blockReports.results.maxPerReport | int | `200` | Max items per PolicyReport resource | +| plugin.kyverno.blockReports.results.keepOnlyLatest | bool | `false` | Keep only the latest of duplicated events | +| plugin.kyverno.imagePullSecrets | list | `[]` | Image pull secrets for image verification policies, this will define the `--imagePullSecrets` argument | +| plugin.kyverno.serviceAccount.create | bool | `true` | Create ServiceAccount | +| plugin.kyverno.serviceAccount.automount | bool | `true` | Enable ServiceAccount automaount | +| plugin.kyverno.serviceAccount.annotations | object | `{}` | Annotations for the ServiceAccount | +| plugin.kyverno.serviceAccount.name | string | `""` | The ServiceAccount name | +| plugin.kyverno.podAnnotations | object | `{}` | Additional annotations to add to each pod | +| plugin.kyverno.podLabels | object | `{}` | Additional labels to add to each pod | +| plugin.kyverno.updateStrategy | object | `{}` | Deployment update strategy. Ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy | +| plugin.kyverno.revisionHistoryLimit | int | `10` | The number of revisions to keep | +| plugin.kyverno.podSecurityContext | object | `{"runAsGroup":1234,"runAsUser":1234}` | Security context for the pod | +| plugin.kyverno.envVars | list | `[]` | Allow additional env variables to be added | +| plugin.kyverno.rbac.enabled | bool | `true` | Create RBAC resources | +| plugin.kyverno.securityContext.runAsUser | int | `1234` | | +| plugin.kyverno.securityContext.runAsNonRoot | bool | `true` | | +| plugin.kyverno.securityContext.privileged | bool | `false` | | +| plugin.kyverno.securityContext.allowPrivilegeEscalation | bool | `false` | | +| plugin.kyverno.securityContext.readOnlyRootFilesystem | bool | `true` | | +| plugin.kyverno.securityContext.capabilities.drop[0] | string | `"ALL"` | | +| plugin.kyverno.securityContext.seccompProfile.type | string | `"RuntimeDefault"` | | +| plugin.kyverno.service.type | string | `"ClusterIP"` | Service type. | +| plugin.kyverno.service.port | int | `8080` | Service port. | +| plugin.kyverno.service.annotations | object | `{}` | Service annotations. | +| plugin.kyverno.service.labels | object | `{}` | Service labels. | +| plugin.kyverno.ingress.enabled | bool | `false` | Create ingress resource. | +| plugin.kyverno.ingress.className | string | `""` | Ingress class name. | +| plugin.kyverno.ingress.labels | object | `{}` | Ingress labels. | +| plugin.kyverno.ingress.annotations | object | `{}` | Ingress annotations. | +| plugin.kyverno.ingress.hosts | list | `[]` | List of ingress host configurations. | +| plugin.kyverno.ingress.tls | list | `[]` | List of ingress TLS configurations. | +| plugin.kyverno.networkPolicy.enabled | bool | `false` | When true, use a NetworkPolicy to allow ingress to the webhook This is useful on clusters using Calico and/or native k8s network policies in a default-deny setup. | +| plugin.kyverno.networkPolicy.egress | list | `[{"ports":[{"port":6443,"protocol":"TCP"}]}]` | A list of valid from selectors according to https://kubernetes.io/docs/concepts/services-networking/network-policies. Enables Kubernetes API Server by default | +| plugin.kyverno.networkPolicy.ingress | list | `[]` | A list of valid from selectors according to https://kubernetes.io/docs/concepts/services-networking/network-policies. | +| plugin.kyverno.resources | object | `{}` | Resource constraints | +| plugin.kyverno.leaderElection.lockName | string | `"kyverno-plugin"` | Lock Name | +| plugin.kyverno.leaderElection.releaseOnCancel | bool | `true` | Released lock when the run context is cancelled. | +| plugin.kyverno.leaderElection.leaseDuration | int | `15` | LeaseDuration is the duration that non-leader candidates will wait to force acquire leadership. | +| plugin.kyverno.leaderElection.renewDeadline | int | `10` | RenewDeadline is the duration that the acting master will retry refreshing leadership before giving up. | +| plugin.kyverno.leaderElection.retryPeriod | int | `2` | RetryPeriod is the duration the LeaderElector clients should wait between tries of actions. | +| plugin.kyverno.podDisruptionBudget.minAvailable | int | `1` | Configures the minimum available pods for kyvernoPlugin disruptions. Cannot be used if `maxUnavailable` is set. | +| plugin.kyverno.podDisruptionBudget.maxUnavailable | string | `nil` | Configures the maximum unavailable pods for kyvernoPlugin disruptions. Cannot be used if `minAvailable` is set. | +| plugin.kyverno.nodeSelector | object | `{}` | Node labels for pod assignment | +| plugin.kyverno.tolerations | list | `[]` | List of node taints to tolerate | +| plugin.kyverno.affinity | object | `{}` | Affinity constraints. | +| plugin.trivy.enabled | bool | `false` | Enable Trivy Operator Plugin | +| plugin.trivy.image.registry | string | `"ghcr.io"` | Image registry | +| plugin.trivy.image.repository | string | `"kyverno/policy-reporter/trivy-plugin"` | Image repository | +| plugin.trivy.image.pullPolicy | string | `"IfNotPresent"` | Image PullPolicy | +| plugin.trivy.image.tag | string | `"0.2.0"` | Image tag Defaults to `Chart.AppVersion` if omitted | +| plugin.trivy.replicaCount | int | `1` | Deployment replica count | +| plugin.trivy.logging.api | bool | `false` | Enables external API request logging | +| plugin.trivy.logging.server | bool | `false` | Enables Server access logging | +| plugin.trivy.logging.encoding | string | `"console"` | log encoding possible encodings are console and json | +| plugin.trivy.logging.logLevel | int | `0` | log level default info | +| plugin.trivy.server.port | int | `8080` | Application port | +| plugin.trivy.policyReporter.skipTLS | bool | `false` | Skip TLS Verification | +| plugin.trivy.policyReporter.certificate | string | `""` | TLS Certificate | +| plugin.trivy.policyReporter.secretRef | string | `""` | Secret to read the API configuration from supports `host`, `certificate`, `skipTLS`, `username`, `password` key | +| plugin.trivy.imagePullSecrets | list | `[]` | Image pull secrets for image verification policies, this will define the `--imagePullSecrets` argument | +| plugin.trivy.serviceAccount.create | bool | `true` | Create ServiceAccount | +| plugin.trivy.serviceAccount.automount | bool | `true` | Enable ServiceAccount automaount | +| plugin.trivy.serviceAccount.annotations | object | `{}` | Annotations for the ServiceAccount | +| plugin.trivy.serviceAccount.name | string | `""` | The ServiceAccount name | +| plugin.trivy.podAnnotations | object | `{}` | Additional annotations to add to each pod | +| plugin.trivy.podLabels | object | `{}` | Additional labels to add to each pod | +| plugin.trivy.updateStrategy | object | `{}` | Deployment update strategy. Ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy | +| plugin.trivy.revisionHistoryLimit | int | `10` | The number of revisions to keep | +| plugin.trivy.podSecurityContext | object | `{"runAsGroup":1234,"runAsUser":1234}` | Security context for the pod | +| plugin.trivy.envVars | list | `[]` | Allow additional env variables to be added | +| plugin.trivy.rbac.enabled | bool | `true` | Create RBAC resources | +| plugin.trivy.securityContext.runAsUser | int | `1234` | | +| plugin.trivy.securityContext.runAsNonRoot | bool | `true` | | +| plugin.trivy.securityContext.privileged | bool | `false` | | +| plugin.trivy.securityContext.allowPrivilegeEscalation | bool | `false` | | +| plugin.trivy.securityContext.readOnlyRootFilesystem | bool | `true` | | +| plugin.trivy.securityContext.capabilities.drop[0] | string | `"ALL"` | | +| plugin.trivy.securityContext.seccompProfile.type | string | `"RuntimeDefault"` | | +| plugin.trivy.service.type | string | `"ClusterIP"` | Service type. | +| plugin.trivy.service.port | int | `8080` | Service port. | +| plugin.trivy.service.annotations | object | `{}` | Service annotations. | +| plugin.trivy.service.labels | object | `{}` | Service labels. | +| plugin.trivy.ingress.enabled | bool | `false` | Create ingress resource. | +| plugin.trivy.ingress.className | string | `""` | Ingress class name. | +| plugin.trivy.ingress.labels | object | `{}` | Ingress labels. | +| plugin.trivy.ingress.annotations | object | `{}` | Ingress annotations. | +| plugin.trivy.ingress.hosts | list | `[]` | List of ingress host configurations. | +| plugin.trivy.ingress.tls | list | `[]` | List of ingress TLS configurations. | +| plugin.trivy.networkPolicy.enabled | bool | `false` | When true, use a NetworkPolicy to allow ingress to the webhook This is useful on clusters using Calico and/or native k8s network policies in a default-deny setup. | +| plugin.trivy.networkPolicy.egress | list | `[{"ports":[{"port":6443,"protocol":"TCP"}]}]` | A list of valid from selectors according to https://kubernetes.io/docs/concepts/services-networking/network-policies. Enables Kubernetes API Server by default | +| plugin.trivy.networkPolicy.ingress | list | `[]` | A list of valid from selectors according to https://kubernetes.io/docs/concepts/services-networking/network-policies. | +| plugin.trivy.resources | object | `{}` | Resource constraints | +| plugin.trivy.podDisruptionBudget.minAvailable | int | `1` | Configures the minimum available pods for kyvernoPlugin disruptions. Cannot be used if `maxUnavailable` is set. | +| plugin.trivy.podDisruptionBudget.maxUnavailable | string | `nil` | Configures the maximum unavailable pods for kyvernoPlugin disruptions. Cannot be used if `minAvailable` is set. | +| plugin.trivy.nodeSelector | object | `{}` | Node labels for pod assignment | +| plugin.trivy.tolerations | list | `[]` | List of node taints to tolerate | +| plugin.trivy.affinity | object | `{}` | Affinity constraints. | +| monitoring.enabled | bool | `false` | Enables the Prometheus Operator integration | +| monitoring.annotations | object | `{}` | Key/value pairs that are attached to all resources. | +| monitoring.serviceMonitor.honorLabels | bool | `false` | HonorLabels chooses the metrics labels on collisions with target labels | +| monitoring.serviceMonitor.namespace | string | `nil` | Allow to override the namespace for serviceMonitor | +| monitoring.serviceMonitor.labels | object | `{}` | Labels to match the serviceMonitorSelector of the Prometheus Resource | +| monitoring.serviceMonitor.relabelings | list | `[]` | ServiceMonitor Relabelings https://github.com/prometheus-operator/prometheus-operator/blob/main/Documentation/api.md#relabelconfig | +| monitoring.serviceMonitor.metricRelabelings | list | `[]` | See serviceMonitor.relabelings | +| monitoring.serviceMonitor.namespaceSelector | optional | `{}` | NamespaceSelector | +| monitoring.serviceMonitor.scrapeTimeout | optional | `nil` | ScrapeTimeout | +| monitoring.serviceMonitor.interval | optional | `nil` | Scrape interval | +| monitoring.grafana.namespace | string | `nil` | Naamespace for configMap of grafana dashboards | +| monitoring.grafana.dashboards.enabled | bool | `true` | Enable the deployment of grafana dashboards | +| monitoring.grafana.dashboards.label | string | `"grafana_dashboard"` | Label to find dashboards using the k8s sidecar | +| monitoring.grafana.dashboards.value | string | `"1"` | Label value to find dashboards using the k8s sidecar | +| monitoring.grafana.dashboards.labelFilter | list | `[]` | List of custom label filter Used to add filter for report label based metric labels defined in custom mode | +| monitoring.grafana.dashboards.multicluster.enabled | bool | `false` | Enable cluster filter in all dashboards | +| monitoring.grafana.dashboards.multicluster.label | string | `"cluster"` | Metric Label which is used to filter clusters | +| monitoring.grafana.dashboards.enable.overview | bool | `true` | Enable the Overview Dashboard | +| monitoring.grafana.dashboards.enable.policyReportDetails | bool | `true` | Enable the PolicyReport Dashboard | +| monitoring.grafana.dashboards.enable.clusterPolicyReportDetails | bool | `true` | Enable the ClusterPolicyReport Dashboard | +| monitoring.grafana.folder.annotation | string | `"grafana_folder"` | Annotation to enable folder storage using the k8s sidecar | +| monitoring.grafana.folder.name | string | `"Policy Reporter"` | Grafana folder in which to store the dashboards | +| monitoring.grafana.datasource.label | string | `"Prometheus"` | Grafana Datasource Label | +| monitoring.grafana.datasource.pluginId | string | `"prometheus"` | Grafana Datasource PluginId | +| monitoring.grafana.datasource.pluginName | string | `"Prometheus"` | Grafana Datasource PluginName | +| monitoring.grafana.grafanaDashboard.enabled | bool | `false` | Create GrafanaDashboard custom resource referencing to the configMap. according to https://grafana-operator.github.io/grafana-operator/docs/examples/dashboard_from_configmap/readme/ | +| monitoring.grafana.grafanaDashboard.folder | string | `"kyverno"` | Dashboard folder | +| monitoring.grafana.grafanaDashboard.allowCrossNamespaceImport | bool | `true` | Allow cross Namespace import | +| monitoring.grafana.grafanaDashboard.matchLabels | object | `{"dashboards":"grafana"}` | Label match selector | +| monitoring.policyReportDetails.firstStatusRow.height | int | `8` | | +| monitoring.policyReportDetails.secondStatusRow.enabled | bool | `true` | | +| monitoring.policyReportDetails.secondStatusRow.height | int | `2` | | +| monitoring.policyReportDetails.statusTimeline.enabled | bool | `true` | | +| monitoring.policyReportDetails.statusTimeline.height | int | `8` | | +| monitoring.policyReportDetails.passTable.enabled | bool | `true` | | +| monitoring.policyReportDetails.passTable.height | int | `8` | | +| monitoring.policyReportDetails.failTable.enabled | bool | `true` | | +| monitoring.policyReportDetails.failTable.height | int | `8` | | +| monitoring.policyReportDetails.warningTable.enabled | bool | `true` | | +| monitoring.policyReportDetails.warningTable.height | int | `4` | | +| monitoring.policyReportDetails.errorTable.enabled | bool | `true` | | +| monitoring.policyReportDetails.errorTable.height | int | `4` | | +| monitoring.clusterPolicyReportDetails.statusRow.height | int | `6` | | +| monitoring.clusterPolicyReportDetails.statusTimeline.enabled | bool | `true` | | +| monitoring.clusterPolicyReportDetails.statusTimeline.height | int | `8` | | +| monitoring.clusterPolicyReportDetails.passTable.enabled | bool | `true` | | +| monitoring.clusterPolicyReportDetails.passTable.height | int | `8` | | +| monitoring.clusterPolicyReportDetails.failTable.enabled | bool | `true` | | +| monitoring.clusterPolicyReportDetails.failTable.height | int | `8` | | +| monitoring.clusterPolicyReportDetails.warningTable.enabled | bool | `true` | | +| monitoring.clusterPolicyReportDetails.warningTable.height | int | `4` | | +| monitoring.clusterPolicyReportDetails.errorTable.enabled | bool | `true` | | +| monitoring.clusterPolicyReportDetails.errorTable.height | int | `4` | | +| monitoring.policyReportOverview.failingSummaryRow.height | int | `8` | | +| monitoring.policyReportOverview.failingTimeline.height | int | `10` | | +| monitoring.policyReportOverview.failingPolicyRuleTable.height | int | `10` | | +| monitoring.policyReportOverview.failingClusterPolicyRuleTable.height | int | `10` | | -* [[Video] 37. #EveryoneCanContribute cafe: Policy reporter for Kyverno](https://youtu.be/1mKywg9f5Fw) -* [[Video] Rawkode Live: Hands on Policy Reporter](https://www.youtube.com/watch?v=ZrOtTELNLyg) -* [[Blog] Monitor Security and Best Practices with Kyverno and Policy Reporter](https://blog.webdev-jogeleit.de/blog/monitor-security-with-kyverno-and-policy-reporter/) +## Source Code + +* + +## Maintainers + +| Name | Email | Url | +| ---- | ------ | --- | +| Frank Jogeleit | | | + +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) diff --git a/charts/policy-reporter/README.md.gotmpl b/charts/policy-reporter/README.md.gotmpl new file mode 100644 index 00000000..86c13c7a --- /dev/null +++ b/charts/policy-reporter/README.md.gotmpl @@ -0,0 +1,50 @@ +{{ template "chart.header" . }} +{{ template "chart.deprecationWarning" . }} +{{ template "chart.description" . }} + +{{ template "chart.badgesSection" . }} + +## Documentation + +You can find detailed Information and Screens about Features and Configurations in the [Documentation](https://kyverno.github.io/policy-reporter-docs). + +## Installation with Helm v3 + +Installation via Helm Repository + +### Add the Helm repository +```bash +helm repo add policy-reporter https://kyverno.github.io/policy-reporter +helm repo update +``` + +### Basic Installation + +The basic installation provides an Prometheus Metrics Endpoint and different REST APIs, for more details have a look at the [Documentation](https://kyverno.github.io/policy-reporter/guide/02-getting-started). + +```bash +helm install policy-reporter policy-reporter/policy-reporter -n policy-reporter --create-namespace +``` + +## Policy Reporter UI + +You can use the Policy Reporter as standalone Application along with the optional UI SubChart. + +### Installation with Policy Reporter UI and Kyverno Plugin enabled + +```bash +helm install policy-reporter policy-reporter/policy-reporter --set plugin.kyverno.enabled=true --set ui.enabled=true -n policy-reporter --create-namespace +kubectl port-forward service/policy-reporter-ui 8082:8080 -n policy-reporter +``` +Open `http://localhost:8082/` in your browser. + + +{{ template "chart.valuesSection" . }} + +{{ template "chart.sourcesSection" . }} + +{{ template "chart.requirementsSection" . }} + +{{ template "chart.maintainersSection" . }} + +{{ template "helm-docs.versionFooter" . }} diff --git a/charts/policy-reporter/charts/kyvernoPlugin/Chart.yaml b/charts/policy-reporter/charts/kyvernoPlugin/Chart.yaml deleted file mode 100644 index b8067688..00000000 --- a/charts/policy-reporter/charts/kyvernoPlugin/Chart.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: v2 -name: kyvernoPlugin -description: Policy Reporter Kyverno Plugin - -type: application -version: 1.6.5 -appVersion: 1.6.3 \ No newline at end of file diff --git a/charts/policy-reporter/charts/kyvernoPlugin/config.yaml b/charts/policy-reporter/charts/kyvernoPlugin/config.yaml deleted file mode 100644 index 428403c7..00000000 --- a/charts/policy-reporter/charts/kyvernoPlugin/config.yaml +++ /dev/null @@ -1,21 +0,0 @@ -blockReports: - {{- toYaml .Values.blockReports | nindent 2 }} - -leaderElection: - enabled: {{ or .Values.leaderElection.enabled (gt (int .Values.replicaCount) 1) }} - releaseOnCancel: {{ .Values.leaderElection.releaseOnCancel }} - leaseDuration: {{ .Values.leaderElection.leaseDuration }} - renewDeadline: {{ .Values.leaderElection.renewDeadline }} - retryPeriod: {{ .Values.leaderElection.retryPeriod }} - -logging: - encoding: {{ .Values.logging.encoding }} - logLevel: {{ include "kyvernoplugin.logLevel" . }} - development: {{ .Values.logging.development }} - -api: - logging: {{ .Values.api.logging }} - basicAuth: - username: {{ .Values.global.basicAuth.username }} - password: {{ .Values.global.basicAuth.password }} - secretRef: {{ .Values.global.basicAuth.secretRef }} diff --git a/charts/policy-reporter/charts/kyvernoPlugin/templates/_helpers.tpl b/charts/policy-reporter/charts/kyvernoPlugin/templates/_helpers.tpl deleted file mode 100644 index a198d3c4..00000000 --- a/charts/policy-reporter/charts/kyvernoPlugin/templates/_helpers.tpl +++ /dev/null @@ -1,105 +0,0 @@ -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -If release name contains chart name it will be used as a full name. -*/}} -{{- define "kyvernoplugin.fullname" -}} -{{- $name := "kyverno-plugin" }} -{{- if .Values.global.fullnameOverride }} -{{- printf "%s-%s" .Values.global.fullnameOverride $name | trunc 63 | trimSuffix "-" }} -{{- else if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} - -{{- define "kyvernoplugin.name" -}} -{{- "kyverno-plugin" }} -{{- end }} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "kyvernoplugin.chart" -}} -{{- printf "kyverno-plugin-%s" .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Common labels -*/}} -{{- define "kyvernoplugin.labels" -}} -helm.sh/chart: {{ include "kyvernoplugin.chart" . }} -{{ include "kyvernoplugin.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/component: plugin -app.kubernetes.io/managed-by: {{ .Release.Service }} -app.kubernetes.io/part-of: policy-reporter -{{- with .Values.global.labels }} -{{ toYaml . }} -{{- end -}} -{{- end }} - -{{/* -Selector labels -*/}} -{{- define "kyvernoplugin.selectorLabels" -}} -app.kubernetes.io/name: {{ include "kyvernoplugin.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - -{{/* -Pod labels -*/}} -{{- define "kyvernoplugin.podLabels" -}} -helm.sh/chart: {{ include "kyvernoplugin.chart" . }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -app.kubernetes.io/part-of: policy-reporter -{{- end }} - -{{/* -Create the name of the service account to use -*/}} -{{- define "kyvernoplugin.serviceAccountName" -}} -{{- if .Values.serviceAccount.create }} -{{- default (include "kyvernoplugin.fullname" .) .Values.serviceAccount.name }} -{{- else }} -{{- default "default" .Values.serviceAccount.name }} -{{- end }} -{{- end }} - -{{/* -Selector labels -*/}} -{{- define "ui.selectorLabels" -}} -app.kubernetes.io/name: ui -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - -{{- define "kyvernoplugin.securityContext" -}} -{{- if semverCompare "<1.19" .Capabilities.KubeVersion.Version }} -{{ toYaml (omit .Values.securityContext "seccompProfile") }} -{{- else }} -{{ toYaml .Values.securityContext }} -{{- end }} -{{- end }} - -{{/* Get the namespace name. */}} -{{- define "kyvernoplugin.namespace" -}} -{{- if .Values.global.namespace -}} - {{- .Values.global.namespace -}} -{{- else -}} - {{- .Release.Namespace -}} -{{- end -}} -{{- end -}} - -{{/* Get the namespace name. */}} -{{- define "kyvernoplugin.logLevel" -}} -{{- if .Values.api.logging -}} --1 -{{- else -}} -{{- .Values.logging.logLevel -}} -{{- end -}} -{{- end -}} diff --git a/charts/policy-reporter/charts/kyvernoPlugin/templates/clusterrolebinding.yaml b/charts/policy-reporter/charts/kyvernoPlugin/templates/clusterrolebinding.yaml deleted file mode 100644 index 168b0a85..00000000 --- a/charts/policy-reporter/charts/kyvernoPlugin/templates/clusterrolebinding.yaml +++ /dev/null @@ -1,20 +0,0 @@ -{{- if and .Values.serviceAccount.create .Values.rbac.enabled -}} -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: {{ include "kyvernoplugin.fullname" . }} - {{- if .Values.annotations }} - annotations: - {{- toYaml .Values.annotations | nindent 4 }} - {{- end }} - labels: - {{- include "kyvernoplugin.labels" . | nindent 4 }} -roleRef: - kind: ClusterRole - name: {{ include "kyvernoplugin.fullname" . }} - apiGroup: rbac.authorization.k8s.io -subjects: -- kind: "ServiceAccount" - name: {{ include "kyvernoplugin.serviceAccountName" . }} - namespace: {{ include "kyvernoplugin.namespace" . }} -{{- end -}} diff --git a/charts/policy-reporter/charts/kyvernoPlugin/templates/config-secret.yaml b/charts/policy-reporter/charts/kyvernoPlugin/templates/config-secret.yaml deleted file mode 100644 index b1152ea4..00000000 --- a/charts/policy-reporter/charts/kyvernoPlugin/templates/config-secret.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: {{ include "kyvernoplugin.fullname" . }}-config - namespace: {{ include "kyvernoplugin.namespace" . }} - {{- if .Values.annotations }} - annotations: - {{- toYaml .Values.annotations | nindent 4 }} - {{- end }} - labels: - {{- include "kyvernoplugin.labels" . | nindent 4 }} -type: Opaque -data: - config.yaml: {{ tpl (.Files.Get "config.yaml") . | b64enc }} diff --git a/charts/policy-reporter/charts/kyvernoPlugin/templates/deployment.yaml b/charts/policy-reporter/charts/kyvernoPlugin/templates/deployment.yaml deleted file mode 100644 index d460cf84..00000000 --- a/charts/policy-reporter/charts/kyvernoPlugin/templates/deployment.yaml +++ /dev/null @@ -1,129 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "kyvernoplugin.fullname" . }} - namespace: {{ include "kyvernoplugin.namespace" . }} - {{- if .Values.annotations }} - annotations: - {{- toYaml .Values.annotations | nindent 4 }} - {{- end }} - labels: - {{- include "kyvernoplugin.labels" . | nindent 4 }} -spec: - replicas: {{ .Values.replicaCount }} - revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} - {{- if .Values.deploymentStrategy }} - strategy: - {{- toYaml .Values.deploymentStrategy | nindent 4 }} - {{- end }} - selector: - matchLabels: - {{- include "kyvernoplugin.selectorLabels" . | nindent 6 }} - template: - metadata: - labels: - {{- include "kyvernoplugin.selectorLabels" . | nindent 8 }} - {{- include "kyvernoplugin.podLabels" . | nindent 8 }} - {{- with .Values.podLabels }} - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.global.labels }} - {{- toYaml . | nindent 8 }} - {{- end }} - annotations: - checksum/secret: {{ include (print .Template.BasePath "/config-secret.yaml") . | sha256sum | quote }} - {{- with .Values.annotations }} - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.podAnnotations }} - {{- toYaml . | nindent 8 }} - {{- end }} - spec: - {{- with .Values.priorityClassName }} - priorityClassName: {{ . }} - {{- end }} - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- if .Values.podSecurityContext }} - securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} - {{- end }} - serviceAccountName: {{ include "kyvernoplugin.serviceAccountName" . }} - automountServiceAccountToken: true - containers: - - name: "kyverno-plugin" - image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - {{- if .Values.securityContext }} - securityContext: {{ include "kyvernoplugin.securityContext" . | nindent 12 }} - {{- end }} - args: - - --port={{ .Values.port.number }} - - --metrics-enabled={{ .Values.metrics.enabled }} - - --rest-enabled={{ .Values.rest.enabled }} - - --lease-name={{ include "kyvernoplugin.fullname" . }} - ports: - - name: {{ .Values.port.name }} - containerPort: {{ .Values.port.number }} - protocol: TCP - livenessProbe: - {{- toYaml .Values.livenessProbe | nindent 12 }} - readinessProbe: - {{- toYaml .Values.readinessProbe | nindent 12 }} - resources: - {{- toYaml .Values.resources | nindent 12 }} - volumeMounts: - - name: config-file - mountPath: /app/config.yaml - subPath: config.yaml - readOnly: true - env: - - name: POD_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - {{- if or .Values.leaderElection.enabled (gt (int .Values.replicaCount) 1) }} - - name: POD_NAME - valueFrom: - fieldRef: - fieldPath: metadata.name - {{- end }} - {{- if .Values.global.basicAuth.secretRef }} - - name: API_AUTH_USERNAME - valueFrom: - secretKeyRef: - name: {{ .Values.global.basicAuth.secretRef }} - key: username - optional: false - - name: API_AUTH_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .Values.global.basicAuth.secretRef }} - key: password - optional: false - {{- end }} - {{- with .Values.envVars }} - {{- . | toYaml | trim | nindent 10 }} - {{- end }} - volumes: - - name: config-file - secret: - secretName: {{ include "kyvernoplugin.fullname" . }}-config - optional: true - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.topologySpreadConstraints }} - topologySpreadConstraints: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} diff --git a/charts/policy-reporter/charts/kyvernoPlugin/templates/ingress.yaml b/charts/policy-reporter/charts/kyvernoPlugin/templates/ingress.yaml deleted file mode 100644 index 23da5ad8..00000000 --- a/charts/policy-reporter/charts/kyvernoPlugin/templates/ingress.yaml +++ /dev/null @@ -1,61 +0,0 @@ -{{- if .Values.ingress.enabled -}} -{{- $fullName := include "kyvernoplugin.fullname" . -}} -{{- $svcPort := .Values.service.port -}} -{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} - {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} - {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} - {{- end }} -{{- end }} -{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} -apiVersion: networking.k8s.io/v1 -{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} -apiVersion: networking.k8s.io/v1beta1 -{{- else -}} -apiVersion: extensions/v1beta1 -{{- end }} -kind: Ingress -metadata: - name: {{ $fullName }} - namespace: {{ include "kyvernoplugin.namespace" . }} - labels: - {{- include "kyvernoplugin.labels" . | nindent 4 }} - {{- if or .Values.annotations .Values.ingress.annotations }} - annotations: - {{- with .Values.ingress.annotations }} - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.annotations }} - {{- toYaml .Values.annotations | nindent 4 }} - {{- end }} - {{- end }} -spec: - {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} - ingressClassName: {{ .Values.ingress.className }} - {{- end }} - {{- if .Values.ingress.tls }} - tls: - {{- toYaml .Values.ingress.tls | nindent 4 }} - {{- end }} - rules: - {{- range .Values.ingress.hosts }} - - host: {{ .host | quote }} - http: - paths: - {{- range .paths }} - - path: {{ .path }} - {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} - pathType: {{ .pathType }} - {{- end }} - backend: - {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} - service: - name: {{ $fullName }} - port: - number: {{ $svcPort }} - {{- else }} - serviceName: {{ $fullName }} - servicePort: {{ $svcPort }} - {{- end }} - {{- end }} - {{- end }} -{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/charts/kyvernoPlugin/templates/networkpolicy.yaml b/charts/policy-reporter/charts/kyvernoPlugin/templates/networkpolicy.yaml deleted file mode 100644 index f65b5c8d..00000000 --- a/charts/policy-reporter/charts/kyvernoPlugin/templates/networkpolicy.yaml +++ /dev/null @@ -1,33 +0,0 @@ -{{- if .Values.networkPolicy.enabled }} -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - labels: {{- include "kyvernoplugin.labels" . | nindent 4 }} - {{- if .Values.annotations }} - annotations: - {{- toYaml .Values.annotations | nindent 4 }} - {{- end }} - name: {{ include "kyvernoplugin.fullname" . }} - namespace: {{ include "kyvernoplugin.namespace" . }} -spec: - podSelector: - matchLabels: {{- include "kyvernoplugin.selectorLabels" . | nindent 6 }} - policyTypes: - - Ingress - - Egress - ingress: - - from: - - podSelector: - matchLabels: - {{- include "ui.selectorLabels" . | nindent 10 }} - ports: - - protocol: TCP - port: 8080 - {{- with .Values.networkPolicy.ingress }} - {{- toYaml . | nindent 2 }} - {{- end }} - {{- with .Values.networkPolicy.egress }} - egress: - {{- toYaml . | nindent 2 }} - {{- end }} -{{- end }} diff --git a/charts/policy-reporter/charts/kyvernoPlugin/templates/poddisruptionbudget.yaml b/charts/policy-reporter/charts/kyvernoPlugin/templates/poddisruptionbudget.yaml deleted file mode 100644 index d876b727..00000000 --- a/charts/policy-reporter/charts/kyvernoPlugin/templates/poddisruptionbudget.yaml +++ /dev/null @@ -1,22 +0,0 @@ -{{- if (gt (int .Values.replicaCount) 1) }} -{{- if .Capabilities.APIVersions.Has "policy/v1/PodDisruptionBudget" }} -apiVersion: policy/v1 -{{- else }} -apiVersion: policy/v1beta1 -{{- end }} -kind: PodDisruptionBudget -metadata: - name: {{ template "kyvernoplugin.fullname" . }} - namespace: {{ include "kyvernoplugin.namespace" . }} - labels: - {{- include "kyvernoplugin.labels" . | nindent 4 }} - {{- if .Values.annotations }} - annotations: - {{- toYaml .Values.annotations | nindent 4 }} - {{- end }} -spec: -{{- include "policyreporter.podDisruptionBudget" . | indent 2 }} - selector: - matchLabels: - {{- include "kyvernoplugin.selectorLabels" . | nindent 6 }} -{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/charts/kyvernoPlugin/templates/role.yaml b/charts/policy-reporter/charts/kyvernoPlugin/templates/role.yaml deleted file mode 100644 index 380ad021..00000000 --- a/charts/policy-reporter/charts/kyvernoPlugin/templates/role.yaml +++ /dev/null @@ -1,24 +0,0 @@ -{{- if and (and .Values.serviceAccount.create .Values.rbac.enabled) (and .Values.blockReports.enabled (or .Values.leaderElection.enabled (gt (int .Values.replicaCount) 1))) -}} -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - {{- if .Values.annotations }} - annotations: - {{- toYaml .Values.annotations | nindent 4 }} - {{- end }} - labels: - {{- include "kyvernoplugin.labels" . | nindent 4 }} - name: {{ include "kyvernoplugin.fullname" . }}-leaderelection - namespace: {{ include "kyvernoplugin.namespace" . }} -rules: -- apiGroups: - - coordination.k8s.io - resources: - - leases - verbs: - - create - - delete - - get - - patch - - update -{{- end -}} \ No newline at end of file diff --git a/charts/policy-reporter/charts/kyvernoPlugin/templates/rolebinding.yaml b/charts/policy-reporter/charts/kyvernoPlugin/templates/rolebinding.yaml deleted file mode 100644 index 9ae8b1f6..00000000 --- a/charts/policy-reporter/charts/kyvernoPlugin/templates/rolebinding.yaml +++ /dev/null @@ -1,21 +0,0 @@ -{{- if and (and .Values.serviceAccount.create .Values.rbac.enabled) (and .Values.blockReports.enabled (or .Values.leaderElection.enabled (gt (int .Values.replicaCount) 1))) -}} -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: {{ include "kyvernoplugin.fullname" . }}-leaderelection - namespace: {{ include "kyvernoplugin.namespace" . }} - {{- if .Values.annotations }} - annotations: - {{- toYaml .Values.annotations | nindent 4 }} - {{- end }} - labels: - {{- include "kyvernoplugin.labels" . | nindent 4 }} -roleRef: - kind: Role - name: {{ include "kyvernoplugin.fullname" . }}-leaderelection - apiGroup: rbac.authorization.k8s.io -subjects: -- kind: "ServiceAccount" - name: {{ include "kyvernoplugin.serviceAccountName" . }} - namespace: {{ include "kyvernoplugin.namespace" . }} -{{- end -}} diff --git a/charts/policy-reporter/charts/kyvernoPlugin/templates/secret-role.yaml b/charts/policy-reporter/charts/kyvernoPlugin/templates/secret-role.yaml deleted file mode 100644 index 1c33ed35..00000000 --- a/charts/policy-reporter/charts/kyvernoPlugin/templates/secret-role.yaml +++ /dev/null @@ -1,19 +0,0 @@ -{{- if and .Values.serviceAccount.create .Values.rbac.enabled -}} -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - {{- if .Values.annotations }} - annotations: - {{- toYaml .Values.annotations | nindent 4 }} - {{- end }} - labels: - {{- include "kyvernoplugin.labels" . | nindent 4 }} - name: {{ include "kyvernoplugin.fullname" . }}-secret-reader - namespace: {{ include "kyvernoplugin.namespace" . }} -rules: -- apiGroups: [''] - resources: - - secrets - verbs: - - get -{{- end -}} \ No newline at end of file diff --git a/charts/policy-reporter/charts/kyvernoPlugin/templates/secret-rolebinding.yaml b/charts/policy-reporter/charts/kyvernoPlugin/templates/secret-rolebinding.yaml deleted file mode 100644 index 26e3363c..00000000 --- a/charts/policy-reporter/charts/kyvernoPlugin/templates/secret-rolebinding.yaml +++ /dev/null @@ -1,21 +0,0 @@ -{{- if and .Values.serviceAccount.create .Values.rbac.enabled -}} -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: {{ include "kyvernoplugin.fullname" . }}-secret-reader - namespace: {{ include "kyvernoplugin.namespace" . }} - {{- if .Values.annotations }} - annotations: - {{- toYaml .Values.annotations | nindent 4 }} - {{- end }} - labels: - {{- include "kyvernoplugin.labels" . | nindent 4 }} -roleRef: - kind: Role - name: {{ include "kyvernoplugin.fullname" . }}-secret-reader - apiGroup: rbac.authorization.k8s.io -subjects: -- kind: "ServiceAccount" - name: {{ include "kyvernoplugin.serviceAccountName" . }} - namespace: {{ include "kyvernoplugin.namespace" . }} -{{- end -}} diff --git a/charts/policy-reporter/charts/kyvernoPlugin/templates/service.yaml b/charts/policy-reporter/charts/kyvernoPlugin/templates/service.yaml deleted file mode 100644 index ef0dc337..00000000 --- a/charts/policy-reporter/charts/kyvernoPlugin/templates/service.yaml +++ /dev/null @@ -1,30 +0,0 @@ -{{- if .Values.service.enabled -}} -apiVersion: v1 -kind: Service -metadata: - name: {{ include "kyvernoplugin.fullname" . }} - namespace: {{ include "kyvernoplugin.namespace" . }} - labels: - {{- include "kyvernoplugin.labels" . | nindent 4 }} - {{- with .Values.service.labels }} - {{- toYaml . | nindent 4 }} - {{- end }} - {{- if or .Values.annotations .Values.service.annotations }} - annotations: - {{- with .Values.annotations }} - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.service.annotations }} - {{- toYaml . | nindent 4 }} - {{- end }} - {{- end }} -spec: - type: {{ .Values.service.type }} - ports: - - port: {{ .Values.service.port }} - targetPort: {{ .Values.port.name }} - protocol: TCP - name: rest - selector: - {{- include "kyvernoplugin.selectorLabels" . | nindent 4 }} -{{- end }} diff --git a/charts/policy-reporter/charts/kyvernoPlugin/templates/serviceaccount.yaml b/charts/policy-reporter/charts/kyvernoPlugin/templates/serviceaccount.yaml deleted file mode 100644 index 79bddf8f..00000000 --- a/charts/policy-reporter/charts/kyvernoPlugin/templates/serviceaccount.yaml +++ /dev/null @@ -1,18 +0,0 @@ -{{- if .Values.serviceAccount.create -}} -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "kyvernoplugin.serviceAccountName" . }} - namespace: {{ include "kyvernoplugin.namespace" . }} - labels: - {{- include "kyvernoplugin.labels" . | nindent 4 }} - {{- if or .Values.annotations .Values.serviceAccount.annotations }} - annotations: - {{- with .Values.annotations }} - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.serviceAccount.annotations }} - {{- toYaml . | nindent 4 }} - {{- end }} - {{- end }} -{{- end }} diff --git a/charts/policy-reporter/charts/kyvernoPlugin/values.yaml b/charts/policy-reporter/charts/kyvernoPlugin/values.yaml deleted file mode 100644 index 3d36363b..00000000 --- a/charts/policy-reporter/charts/kyvernoPlugin/values.yaml +++ /dev/null @@ -1,211 +0,0 @@ -image: - registry: ghcr.io - repository: kyverno/policy-reporter-kyverno-plugin - pullPolicy: IfNotPresent - tag: 1.6.3 - -imagePullSecrets: [] - -priorityClassName: "" - -replicaCount: 1 - -revisionHistoryLimit: 10 - -deploymentStrategy: {} - # rollingUpdate: - # maxSurge: 25% - # maxUnavailable: 25% - # type: RollingUpdate - -# When using a custom port together with the PolicyReporter UI -# the port has also to be changed in the UI subchart as well because it can't access values of other subcharts. -# You can change the port under `ui.kyvernoPlugin.port` -port: - name: rest - number: 8080 - -# Key/value pairs that are attached to all resources. -annotations: {} - -# Create cluster role policies -rbac: - enabled: true - -serviceAccount: - # Specifies whether a service account should be created - create: true - # Annotations to add to the service account - annotations: {} - # The name of the service account to use. - # If not set and create is true, a name is generated using the fullname template - name: "" - -service: - enabled: true - ## configuration of service - # key/value - annotations: {} - # key/value - labels: {} - port: 8080 - type: ClusterIP - -## Set to true to enable ingress record generation -# ref to: https://kubernetes.io/docs/concepts/services-networking/ingress/ -ingress: - enabled: false - className: "" - # key/value - labels: {} - # key/value - annotations: {} - # kubernetes.io/ingress.class: nginx - # kubernetes.io/tls-acme: "true" - hosts: - - host: chart-example.local - paths: [] - tls: [] - # - secretName: chart-example-tls - # hosts: - # - chart-example.local -podSecurityContext: - runAsUser: 1234 - runAsGroup: 1234 - -securityContext: - runAsUser: 1234 - runAsNonRoot: true - privileged: false - allowPrivilegeEscalation: false - readOnlyRootFilesystem: true - capabilities: - drop: - - ALL - seccompProfile: - type: RuntimeDefault - -# Key/value pairs that are attached to pods. -podAnnotations: {} - -# Key/value pairs that are attached to pods. -podLabels: {} - -# Allow additional env variables to be added -envVars: [] - -resources: {} - # We usually recommend not to specify default resources and to leave this as a conscious - # choice for the user. This also increases chances charts run on environments with little - # resources, such as Minikube. If you do want to specify resources, uncomment the following - # lines, adjust them as necessary, and remove the curly braces after 'resources:'. - # limits: - # memory: 30Mi - # cpu: 10m - # requests: - # memory: 20Mi - # cpu: 5m - -# Node labels for pod assignment -# ref: https://kubernetes.io/docs/user-guide/node-selection/ -nodeSelector: {} - -# Tolerations for pod assignment -# ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ -tolerations: [] - -# Anti-affinity to disallow deploying client and master nodes on the same worker node -affinity: {} - -# Topology Spread Constraints to better spread pods -topologySpreadConstraints: [] - -# livenessProbe for policy-reporter-kyverno-plugin -livenessProbe: - httpGet: - path: /healthz - port: rest - -# readinessProbe for policy-reporter-kyverno-plugin -readinessProbe: - httpGet: - path: /ready - port: rest - -# REST API -rest: - enabled: true - -# Prometheus Metrics API -metrics: - enabled: true - -logging: - encoding: console # possible encodings are console and json - logLevel: 0 # default info - development: false # more human readable structure, enables stacktraces and removes log sampling - -api: - logging: false # enable debug API access logging, sets logLevel to debug - -# create PolicyReports for enforce policies, -# based on Events created by Kyverno (>= v1.7.0) -blockReports: - enabled: false - eventNamespace: default - results: - maxPerReport: 200 - keepOnlyLatest: false - -# required if policy-reporter-kyverno-plugin should run in HA mode and the "blockReports" feature is enabled -# if "blockReports" is disabled, leaderElection is also disabled automatically -# will be enabled when replicaCount > 1 -leaderElection: - enabled: false - releaseOnCancel: true - leaseDuration: 15 - renewDeadline: 10 - retryPeriod: 2 - -# enabled if replicaCount > 1 -podDisruptionBudget: - # -- Configures the minimum available pods for kyvernoPlugin disruptions. - # Cannot be used if `maxUnavailable` is set. - minAvailable: 1 - # -- Configures the maximum unavailable pods for kyvernoPlugin disruptions. - # Cannot be used if `minAvailable` is set. - maxUnavailable: - -# Enable a NetworkPolicy for this chart. Useful on clusters where Network Policies are -# used and configured in a default-deny fashion. -networkPolicy: - enabled: false - # Kubernetes API Server - egress: - - to: - ports: - - protocol: TCP - port: 6443 - ingress: [] - -# Should be set in the parent chart only -global: - # available plugins - plugins: - # enable kyverno for Policy Reporter UI and monitoring - kyverno: false - # overwrite the fullname of all resources including subcharts - fullnameOverride: "" - # configure the namespace of all resources including subcharts - namespace: "" - # additional labels added on each resource - labels: {} - # basicAuth for APIs and metrics - basicAuth: - # HTTP BasicAuth username - username: "" - # HTTP BasicAuth password - password: "" - # read credentials from secret - secretRef: "" - diff --git a/charts/policy-reporter/charts/monitoring/Chart.yaml b/charts/policy-reporter/charts/monitoring/Chart.yaml deleted file mode 100644 index d0f0ff1e..00000000 --- a/charts/policy-reporter/charts/monitoring/Chart.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: v2 -name: monitoring -description: Policy Reporter Monitoring with predefined ServiceMonitor and Grafana Dashboards - -type: application -version: 2.8.2 -appVersion: 0.0.0 diff --git a/charts/policy-reporter/charts/monitoring/templates/_helpers.tpl b/charts/policy-reporter/charts/monitoring/templates/_helpers.tpl deleted file mode 100644 index 25404c02..00000000 --- a/charts/policy-reporter/charts/monitoring/templates/_helpers.tpl +++ /dev/null @@ -1,85 +0,0 @@ -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -If release name contains chart name it will be used as a full name. -*/}} -{{- define "monitoring.fullname" -}} -{{- $name := default .Chart.Name .Values.nameOverride }} -{{- if .Values.global.fullnameOverride }} -{{- printf "%s-%s" .Values.global.fullnameOverride $name | trunc 63 | trimSuffix "-" }} -{{- else if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "monitoring.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Common labels -*/}} -{{- define "monitoring.labels" -}} -helm.sh/chart: {{ include "monitoring.chart" . }} -{{ include "monitoring.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/component: monitoring -app.kubernetes.io/managed-by: {{ .Release.Service }} -app.kubernetes.io/part-of: kyverno -{{- with .Values.global.labels }} -{{ toYaml . }} -{{- end -}} -{{- end }} - -{{/* -Selector labels -*/}} -{{- define "monitoring.selectorLabels" -}} -app.kubernetes.io/name: {{ include "monitoring.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - -{{- define "monitoring.name" -}} -{{- "monitoring" }} -{{- end }} - -{{- define "monitoring.namespace" -}} -{{- if .Values.grafana.namespace -}} -{{- .Values.grafana.namespace -}} -{{- else if .Values.global.namespace -}} - {{- .Values.global.namespace -}} -{{- else -}} -{{- .Release.Namespace -}} -{{- end }} -{{- end }} - -{{- define "kyvernoplugin.selectorLabels" -}} -app.kubernetes.io/name: kyverno-plugin -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - -{{/* Get the namespace name. */}} -{{- define "monitoring.smNamespace" -}} -{{- if .Values.serviceMonitor.namespace -}} -{{- .Values.serviceMonitor.namespace -}} -{{- else if .Values.global.namespace -}} - {{- .Values.global.namespace -}} -{{- else -}} -{{- .Release.Namespace -}} -{{- end }} -{{- end }} - -{{/* -Policy Reporter Selector labels -*/}} -{{- define "policyreporter.selectorLabels" -}} -app.kubernetes.io/name: policy-reporter -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} diff --git a/charts/policy-reporter/charts/monitoring/templates/auth-secret.yaml b/charts/policy-reporter/charts/monitoring/templates/auth-secret.yaml deleted file mode 100644 index 6737f023..00000000 --- a/charts/policy-reporter/charts/monitoring/templates/auth-secret.yaml +++ /dev/null @@ -1,17 +0,0 @@ -{{- if and .Values.global.basicAuth.username .Values.global.basicAuth.password }} -apiVersion: v1 -kind: Secret -metadata: - name: {{ include "monitoring.fullname" . }}-auth - namespace: {{ include "monitoring.smNamespace" . }} - {{- if .Values.annotations }} - annotations: - {{- toYaml .Values.annotations | nindent 4 }} - {{- end }} - labels: - {{- include "monitoring.labels" . | nindent 4 }} -type: Opaque -data: - username: {{ .Values.global.basicAuth.username | b64enc }} - password: {{ .Values.global.basicAuth.password | b64enc }} -{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/charts/monitoring/templates/clusterpolicy-details.grafanadashboard.yaml b/charts/policy-reporter/charts/monitoring/templates/clusterpolicy-details.grafanadashboard.yaml deleted file mode 100644 index 48d11793..00000000 --- a/charts/policy-reporter/charts/monitoring/templates/clusterpolicy-details.grafanadashboard.yaml +++ /dev/null @@ -1,20 +0,0 @@ -{{- if and $.Values.grafana.dashboards.enabled $.Values.grafana.dashboards.enable.clusterPolicyReportDetails $.Values.grafana.grafanaDashboard.enabled }} ---- -apiVersion: grafana.integreatly.org/v1beta1 -kind: GrafanaDashboard -metadata: - labels: - {{ .Values.grafana.dashboards.label }}: {{ .Values.grafana.dashboards.value | quote }} - {{- include "monitoring.labels" . | nindent 4 }} - name: {{ include "monitoring.fullname" . }}-clusterpolicy-details-dashboard - namespace: {{ include "monitoring.namespace" . }} -spec: - allowCrossNamespaceImport: {{ $.Values.grafana.grafanaDashboard.allowCrossNamespaceImport }} - folder: {{ $.Values.grafana.grafanaDashboard.folder }} - instanceSelector: - matchLabels: - {{- toYaml $.Values.grafana.grafanaDashboard.matchLabels | nindent 6 }} - configMapRef: - name: {{ include "monitoring.fullname" . }}-clusterpolicy-details-dashboard - key: cluster-policy-reporter-details-dashboard.json -{{- end }} diff --git a/charts/policy-reporter/charts/monitoring/templates/kyverno-servicemonitor.yaml b/charts/policy-reporter/charts/monitoring/templates/kyverno-servicemonitor.yaml deleted file mode 100644 index 4ad1c816..00000000 --- a/charts/policy-reporter/charts/monitoring/templates/kyverno-servicemonitor.yaml +++ /dev/null @@ -1,63 +0,0 @@ -{{- if or .Values.plugins.kyverno .Values.global.plugins.kyverno -}} -apiVersion: monitoring.coreos.com/v1 -kind: ServiceMonitor -metadata: - name: {{ include "monitoring.fullname" . }}-kyverno-plugin - namespace: {{ include "monitoring.smNamespace" . }} - {{- if .Values.annotations }} - annotations: - {{- toYaml .Values.annotations | nindent 4 }} - {{- end }} - labels: - {{- with .Values.serviceMonitor.labels }} - {{- toYaml . | nindent 4 }} - {{- end }} - {{- include "monitoring.labels" . | nindent 4 }} -spec: - selector: - matchLabels: - {{- include "kyvernoplugin.selectorLabels" . | nindent 8 }} - {{- with .Values.kyverno.serviceMonitor.namespaceSelector }} - namespaceSelector: - {{- toYaml . | nindent 4 }} - {{- end }} - endpoints: - - port: rest - {{- if and .Values.global.basicAuth.username .Values.global.basicAuth.password }} - basicAuth: - password: - name: {{ include "monitoring.fullname" . }}-auth - key: password - username: - name: {{ include "monitoring.fullname" . }}-auth - key: username - {{- else if .Values.global.basicAuth.secretRef }} - basicAuth: - password: - name: {{ .Values.global.basicAuth.secretRef }} - key: password - username: - name: {{ .Values.global.basicAuth.secretRef }} - key: username - {{- end }} - honorLabels: {{ .Values.kyverno.serviceMonitor.honorLabels }} - {{- if .Values.kyverno.serviceMonitor.scrapeTimeout }} - scrapeTimeout: {{ .Values.kyverno.serviceMonitor.scrapeTimeout }} - {{- end }} - {{- if .Values.kyverno.serviceMonitor.interval }} - interval: {{ .Values.kyverno.serviceMonitor.interval }} - {{- end }} - relabelings: - - action: labeldrop - regex: pod|service|container - - targetLabel: instance - replacement: policy-reporter - action: replace - {{- with .Values.kyverno.serviceMonitor.relabelings }} - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.kyverno.serviceMonitor.metricRelabelings }} - metricRelabelings: - {{- toYaml . | nindent 4 }} - {{- end }} -{{- end }} diff --git a/charts/policy-reporter/charts/monitoring/templates/overview.grafanadashboard.yaml b/charts/policy-reporter/charts/monitoring/templates/overview.grafanadashboard.yaml deleted file mode 100644 index b4a29b6f..00000000 --- a/charts/policy-reporter/charts/monitoring/templates/overview.grafanadashboard.yaml +++ /dev/null @@ -1,20 +0,0 @@ -{{- if and $.Values.grafana.dashboards.enabled $.Values.grafana.dashboards.enable.overview $.Values.grafana.grafanaDashboard.enabled }} ---- -apiVersion: grafana.integreatly.org/v1beta1 -kind: GrafanaDashboard -metadata: - labels: - {{ .Values.grafana.dashboards.label }}: {{ .Values.grafana.dashboards.value | quote }} - {{- include "monitoring.labels" . | nindent 4 }} - name: {{ include "monitoring.fullname" . }}-overview-dashboard - namespace: {{ include "monitoring.namespace" . }} -spec: - allowCrossNamespaceImport: {{ $.Values.grafana.grafanaDashboard.allowCrossNamespaceImport }} - folder: {{ $.Values.grafana.grafanaDashboard.folder }} - instanceSelector: - matchLabels: - {{- toYaml $.Values.grafana.grafanaDashboard.matchLabels | nindent 6 }} - configMapRef: - name: {{ include "monitoring.fullname" . }}-overview-dashboard - key: policy-reporter-dashboard.json -{{- end }} diff --git a/charts/policy-reporter/charts/monitoring/templates/policy-details.grafanadashboard.yaml b/charts/policy-reporter/charts/monitoring/templates/policy-details.grafanadashboard.yaml deleted file mode 100644 index c4a5b9f7..00000000 --- a/charts/policy-reporter/charts/monitoring/templates/policy-details.grafanadashboard.yaml +++ /dev/null @@ -1,20 +0,0 @@ -{{- if and $.Values.grafana.dashboards.enabled $.Values.grafana.dashboards.enable.policyReportDetails $.Values.grafana.grafanaDashboard.enabled }} ---- -apiVersion: grafana.integreatly.org/v1beta1 -kind: GrafanaDashboard -metadata: - labels: - {{ .Values.grafana.dashboards.label }}: {{ .Values.grafana.dashboards.value | quote }} - {{- include "monitoring.labels" . | nindent 4 }} - name: {{ include "monitoring.fullname" . }}-policy-details-dashboard - namespace: {{ include "monitoring.namespace" . }} -spec: - allowCrossNamespaceImport: {{ $.Values.grafana.grafanaDashboard.allowCrossNamespaceImport }} - folder: {{ $.Values.grafana.grafanaDashboard.folder }} - instanceSelector: - matchLabels: - {{- toYaml $.Values.grafana.grafanaDashboard.matchLabels | nindent 6 }} - configMapRef: - name: {{ include "monitoring.fullname" . }}-policy-details-dashboard - key: policy-reporter-details-dashboard.json -{{- end }} diff --git a/charts/policy-reporter/charts/monitoring/values.yaml b/charts/policy-reporter/charts/monitoring/values.yaml deleted file mode 100644 index 2c7c4fcb..00000000 --- a/charts/policy-reporter/charts/monitoring/values.yaml +++ /dev/null @@ -1,150 +0,0 @@ -# Override the chart name used for all resources -nameOverride: "" - -plugins: - kyverno: false - -# Key/value pairs that are attached to all resources. -annotations: {} - -serviceMonitor: - # HonorLabels chooses the metrics labels on collisions with target labels - honorLabels: false - # allow to override the namespace for serviceMonitor - namespace: - # labels to match the serviceMonitorSelector of the Prometheus Resource - labels: {} - # https://github.com/prometheus-operator/prometheus-operator/blob/main/Documentation/api.md#relabelconfig - relabelings: [] - # see serviceMonitor.relabelings - metricRelabelings: [] - # optional namespaceSelector - namespaceSelector: {} - # optional scrapeTimeout - scrapeTimeout: - # optional scrape interval - interval: - -kyverno: - serviceMonitor: - # HonorLabels chooses the metrics labels on collisions with target labels - honorLabels: false - # see serviceMonitor.relabelings - relabelings: [] - # see serviceMonitor.relabelings - metricRelabelings: [] - # optional namespaceSelector - namespaceSelector: {} - # optional scrapeTimeout - scrapeTimeout: - # optional scrape interval - interval: - -grafana: - # namespace for configMap of grafana dashboards - namespace: - dashboards: - # Enable the deployment of grafana dashboards - enabled: true - # Label to find dashboards using the k8s sidecar - label: grafana_dashboard - value: "1" - labelFilter: [] - multicluster: - enabled: false - label: cluster - enable: - overview: true - policyReportDetails: true - clusterPolicyReportDetails: true - folder: - # Annotation to enable folder storage using the k8s sidecar - annotation: grafana_folder - # Grafana folder in which to store the dashboards - name: Policy Reporter - datasource: - label: Prometheus - pluginId: prometheus - pluginName: Prometheus - - # -- create GrafanaDashboard custom resource referencing to the configMap. - # according to https://grafana-operator.github.io/grafana-operator/docs/examples/dashboard_from_configmap/readme/ - grafanaDashboard: - enabled: false - folder: kyverno - allowCrossNamespaceImport: true - matchLabels: - dashboards: "grafana" - - -policyReportDetails: - firstStatusRow: - height: 8 - secondStatusRow: - enabled: true - height: 2 - statusTimeline: - enabled: true - height: 8 - passTable: - enabled: true - height: 8 - failTable: - enabled: true - height: 8 - warningTable: - enabled: true - height: 4 - errorTable: - enabled: true - height: 4 - -clusterPolicyReportDetails: - statusRow: - height: 6 - statusTimeline: - enabled: true - height: 8 - passTable: - enabled: true - height: 8 - failTable: - enabled: true - height: 8 - warningTable: - enabled: true - height: 4 - errorTable: - enabled: true - height: 4 - -policyReportOverview: - failingSummaryRow: - height: 8 - failingTimeline: - height: 10 - failingPolicyRuleTable: - height: 10 - failingClusterPolicyRuleTable: - height: 10 - -# Should be set in the parent chart only -global: - # available plugins - plugins: - # enable kyverno for Policy Reporter UI and monitoring - kyverno: false - # overwrite the fullname of all resources including subcharts - fullnameOverride: "" - # configure the namespace of all resources including subcharts - namespace: "" - # additional labels added on each resource - labels: {} - # basicAuth for APIs and metrics - basicAuth: - # HTTP BasicAuth username - username: "" - # HTTP BasicAuth password - password: "" - # read credentials from secret - secretRef: "" diff --git a/charts/policy-reporter/charts/ui/Chart.yaml b/charts/policy-reporter/charts/ui/Chart.yaml deleted file mode 100644 index 7cca02c9..00000000 --- a/charts/policy-reporter/charts/ui/Chart.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: v2 -name: ui -description: Policy Reporter UI - -type: application -version: 2.10.5 -appVersion: 1.9.2 diff --git a/charts/policy-reporter/charts/ui/templates/_helpers.tpl b/charts/policy-reporter/charts/ui/templates/_helpers.tpl deleted file mode 100644 index cf814751..00000000 --- a/charts/policy-reporter/charts/ui/templates/_helpers.tpl +++ /dev/null @@ -1,140 +0,0 @@ -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -If release name contains chart name it will be used as a full name. -*/}} -{{- define "ui.fullname" -}} -{{- $name := "ui" }} -{{- if .Values.global.fullnameOverride }} -{{- printf "%s-%s" .Values.global.fullnameOverride $name | trunc 63 | trimSuffix "-" }} -{{- else if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} - -{{- define "ui.name" -}} -{{- "ui" }} -{{- end }} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "ui.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Common labels -*/}} -{{- define "ui.labels" -}} -helm.sh/chart: {{ include "ui.chart" . }} -{{ include "ui.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/component: ui -app.kubernetes.io/managed-by: {{ .Release.Service }} -app.kubernetes.io/part-of: policy-reporter -{{- with .Values.global.labels }} -{{ toYaml . }} -{{- end -}} -{{- with .Values.ingress.labels }} -{{ toYaml . }} -{{- end -}} -{{- end }} - -{{/* -Selector labels -*/}} -{{- define "ui.selectorLabels" -}} -app.kubernetes.io/name: {{ include "ui.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - -{{/* -Pod labels -*/}} -{{- define "ui.podLabels" -}} -helm.sh/chart: {{ include "ui.chart" . }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -app.kubernetes.io/part-of: policy-reporter -{{- end }} - -{{/* -Policy Reporter Selector labels -*/}} -{{- define "policyreporter.selectorLabels" -}} -app.kubernetes.io/name: policy-reporter -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - -{{/* -Kyverno Plugin Selector labels -*/}} -{{- define "kyvernoplugin.selectorLabels" -}} -app.kubernetes.io/name: kyverno-plugin -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - -{{/* -Create the name of the service account to use -*/}} -{{- define "ui.serviceAccountName" -}} -{{- if .Values.serviceAccount.create }} -{{- default (include "ui.fullname" .) .Values.serviceAccount.name }} -{{- else }} -{{- default "default" .Values.serviceAccount.name }} -{{- end }} -{{- end }} - -{{- define "ui.kyvernoPluginServiceName" -}} -{{- $name := "kyverno-plugin" }} -{{- if .Values.global.fullnameOverride }} -{{- printf "%s-%s" .Values.global.fullnameOverride $name | trunc 63 | trimSuffix "-" }} -{{- else if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} - -{{- define "ui.policyReportServiceName" -}} -{{- $name := "policy-reporter" }} -{{- if .Values.global.backend }} -{{- .Values.global.backend }} -{{- else if .Values.global.fullnameOverride }} -{{- .Values.global.fullnameOverride }} -{{- else if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} - -{{- define "ui.securityContext" -}} -{{- if semverCompare "<1.19" .Capabilities.KubeVersion.Version }} -{{ toYaml (omit .Values.securityContext "seccompProfile") }} -{{- else }} -{{ toYaml .Values.securityContext }} -{{- end }} -{{- end }} - -{{/* Get the namespace name. */}} -{{- define "ui.namespace" -}} -{{- if .Values.global.namespace -}} - {{- .Values.global.namespace -}} -{{- else -}} - {{- .Release.Namespace -}} -{{- end -}} -{{- end -}} - -{{/* Get the namespace name. */}} -{{- define "ui.logLevel" -}} -{{- if .Values.api.logging -}} --1 -{{- else -}} -{{- .Values.logging.logLevel -}} -{{- end -}} -{{- end -}} diff --git a/charts/policy-reporter/charts/ui/templates/config.yaml b/charts/policy-reporter/charts/ui/templates/config.yaml deleted file mode 100644 index eef0df74..00000000 --- a/charts/policy-reporter/charts/ui/templates/config.yaml +++ /dev/null @@ -1,52 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ include "ui.fullname" . }}-config - namespace: {{ include "ui.namespace" . }} - {{- if .Values.annotations }} - annotations: - {{- toYaml .Values.annotations | nindent 4 }} - {{- end }} - labels: - {{- include "ui.labels" . | nindent 4 }} -data: - config.yaml: |- - logSize: {{ .Values.log.size }} - displayMode: {{ .Values.displayMode | quote }} - refreshInterval: {{ .Values.refreshInterval }} - clusterName: {{ .Values.clusterName | quote }} - views: - dashboard: - policyReports: {{ .Values.views.dashboard.policyReports }} - clusterPolicyReports: {{ .Values.views.dashboard.clusterPolicyReports }} - logs: {{ .Values.views.logs }} - policyReports: {{ .Values.views.policyReports }} - clusterPolicyReports: {{ .Values.views.clusterPolicyReports }} - kyvernoPolicies: {{ .Values.views.kyvernoPolicies }} - kyvernoVerifyImages: {{ .Values.views.kyvernoVerifyImages }} - {{- with .Values.clusters }} - clusters: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.labelFilter }} - labelFilter: - {{- toYaml . | nindent 4 }} - {{- end }} - - {{- with .Values.redis }} - redis: - {{- toYaml . | nindent 6 }} - {{- end }} - - logging: - encoding: {{ .Values.logging.encoding }} - logLevel: {{ include "ui.logLevel" . }} - development: {{ .Values.logging.development }} - - apiConfig: - logging: {{ .Values.api.logging }} - overwriteHost: {{ .Values.api.overwriteHost }} - basicAuth: - username: {{ .Values.global.basicAuth.username }} - password: {{ .Values.global.basicAuth.password }} - secretRef: {{ .Values.global.basicAuth.secretRef }} \ No newline at end of file diff --git a/charts/policy-reporter/charts/ui/templates/deployment.yaml b/charts/policy-reporter/charts/ui/templates/deployment.yaml deleted file mode 100644 index 701ba65c..00000000 --- a/charts/policy-reporter/charts/ui/templates/deployment.yaml +++ /dev/null @@ -1,123 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "ui.fullname" . }} - namespace: {{ include "ui.namespace" . }} - labels: - {{- include "ui.labels" . | nindent 4 }} - {{- if .Values.annotations }} - annotations: - {{- toYaml .Values.annotations | nindent 4 }} - {{- end }} -spec: - replicas: {{ .Values.replicaCount }} - revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} - {{- if .Values.deploymentStrategy }} - strategy: - {{- toYaml .Values.deploymentStrategy | nindent 4 }} - {{- end }} - selector: - matchLabels: - {{- include "ui.selectorLabels" . | nindent 6 }} - template: - metadata: - labels: - {{- include "ui.selectorLabels" . | nindent 8 }} - {{- include "ui.podLabels" . | nindent 8 }} - {{- with .Values.podLabels }} - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.global.labels }} - {{- toYaml . | nindent 8 }} - {{- end }} - annotations: - checksum/config: {{ include (print .Template.BasePath "/config.yaml") . | sha256sum | quote }} - {{- with .Values.podAnnotations }} - {{- toYaml . | nindent 8 }} - {{- end }} - spec: - {{- with .Values.priorityClassName }} - priorityClassName: {{ . }} - {{- end }} - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - serviceAccountName: {{ include "ui.serviceAccountName" . }} - automountServiceAccountToken: true - containers: - - name: {{ default .Chart.Name .Values.nameOverride }} - image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - {{- if .Values.securityContext }} - securityContext: {{ include "ui.securityContext" . | nindent 12 }} - {{- end }} - args: - - -config=/app/config.yaml - - -policy-reporter=http://{{ include "ui.policyReportServiceName" . }}:{{ .Values.policyReporter.port }} - {{- if or .Values.plugins.kyverno .Values.global.plugins.kyverno }} - - -kyverno-plugin=http://{{ include "ui.kyvernoPluginServiceName" . }}:{{ .Values.kyvernoPlugin.port }} - {{- end }} - ports: - - name: http - containerPort: 8080 - protocol: TCP - livenessProbe: - httpGet: - path: / - port: http - readinessProbe: - httpGet: - path: / - port: http - volumeMounts: - - name: config-file - mountPath: /app/config.yaml - subPath: config.yaml - readOnly: true - {{- if .Values.volumes }} - {{- toYaml .Values.volumeMounts | nindent 10 }} - {{- end }} - resources: - {{- toYaml .Values.resources | nindent 12 }} - env: - - name: POD_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - {{- with .Values.envVars }} - {{- . | toYaml | trim | nindent 10 }} - {{- end }} - {{- if .Values.sidecarContainers }} - {{- range $name, $spec := .Values.sidecarContainers }} - - name: {{ $name }} - {{- if kindIs "string" $spec }} - {{- tpl $spec $ | nindent 10 }} - {{- else }} - {{- toYaml $spec | nindent 10 }} - {{- end }} - {{- end }} - {{- end }} - volumes: - - name: config-file - configMap: - name: {{ include "ui.fullname" . }}-config - {{- if .Values.volumes }} - {{- toYaml .Values.volumes | nindent 6 }} - {{- end }} - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.topologySpreadConstraints }} - topologySpreadConstraints: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} diff --git a/charts/policy-reporter/charts/ui/templates/extra-manifests.yaml b/charts/policy-reporter/charts/ui/templates/extra-manifests.yaml deleted file mode 100644 index 9059d7d0..00000000 --- a/charts/policy-reporter/charts/ui/templates/extra-manifests.yaml +++ /dev/null @@ -1,4 +0,0 @@ -{{ range .Values.extraManifests }} ---- -{{ tpl . $ }} -{{ end }} \ No newline at end of file diff --git a/charts/policy-reporter/charts/ui/templates/networkpolicy.yaml b/charts/policy-reporter/charts/ui/templates/networkpolicy.yaml deleted file mode 100644 index 42650085..00000000 --- a/charts/policy-reporter/charts/ui/templates/networkpolicy.yaml +++ /dev/null @@ -1,44 +0,0 @@ -{{- if .Values.networkPolicy.enabled }} -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: {{ include "ui.fullname" . }} - namespace: {{ include "ui.namespace" . }} - {{- if .Values.annotations }} - annotations: - {{- toYaml .Values.annotations | nindent 4 }} - {{- end }} - labels: - {{- include "ui.labels" . | nindent 4 }} -spec: - podSelector: - matchLabels: {{- include "ui.selectorLabels" . | nindent 6 }} - policyTypes: - - Ingress - - Egress - ingress: - - from: - ports: - - protocol: TCP - port: {{ .Values.service.port }} - egress: - - to: - - podSelector: - matchLabels: - {{- include "policyreporter.selectorLabels" . | nindent 10 }} - ports: - - protocol: TCP - port: 8080 - {{- if or .Values.plugins.kyverno .Values.global.plugins.kyverno }} - - to: - - podSelector: - matchLabels: - {{- include "kyvernoplugin.selectorLabels" . | nindent 10 }} - ports: - - protocol: TCP - port: 8080 - {{- end }} - {{- with .Values.networkPolicy.egress }} - {{- toYaml . | nindent 2 }} - {{- end }} -{{- end }} diff --git a/charts/policy-reporter/charts/ui/templates/service.yaml b/charts/policy-reporter/charts/ui/templates/service.yaml deleted file mode 100644 index d413677d..00000000 --- a/charts/policy-reporter/charts/ui/templates/service.yaml +++ /dev/null @@ -1,33 +0,0 @@ -{{- if .Values.service.enabled -}} -apiVersion: v1 -kind: Service -metadata: - name: {{ include "ui.fullname" . }} - namespace: {{ include "ui.namespace" . }} - labels: - {{- include "ui.labels" . | nindent 4 }} - {{- with .Values.service.labels }} - {{- toYaml . | nindent 4 }} - {{- end }} - {{- if or .Values.annotations .Values.service.annotations }} - annotations: - {{- with .Values.annotations }} - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.service.annotations }} - {{- toYaml . | nindent 4 }} - {{- end }} - {{- end }} -spec: - type: {{ .Values.service.type }} - ports: - - port: {{ .Values.service.port }} - targetPort: http - protocol: TCP - name: http -{{- if .Values.service.additionalPorts }} -{{ toYaml .Values.service.additionalPorts | indent 4 }} -{{- end }} - selector: - {{- include "ui.selectorLabels" . | nindent 4 }} -{{- end }} diff --git a/charts/policy-reporter/charts/ui/templates/serviceaccount.yaml b/charts/policy-reporter/charts/ui/templates/serviceaccount.yaml deleted file mode 100644 index ce349a1e..00000000 --- a/charts/policy-reporter/charts/ui/templates/serviceaccount.yaml +++ /dev/null @@ -1,18 +0,0 @@ -{{- if .Values.serviceAccount.create -}} -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "ui.serviceAccountName" . }} - namespace: {{ include "ui.namespace" . }} - labels: - {{- include "ui.labels" . | nindent 4 }} - {{- if or .Values.annotations .Values.serviceAccount.annotations }} - annotations: - {{- with .Values.annotations }} - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.serviceAccount.annotations }} - {{- toYaml . | nindent 4 }} - {{- end }} - {{- end }} -{{- end }} diff --git a/charts/policy-reporter/charts/ui/values.yaml b/charts/policy-reporter/charts/ui/values.yaml deleted file mode 100644 index d3e0a189..00000000 --- a/charts/policy-reporter/charts/ui/values.yaml +++ /dev/null @@ -1,279 +0,0 @@ -enabled: false - -# Override the chart name used for all resources -nameOverride: "" - -priorityClassName: "" - -image: - registry: ghcr.io - repository: kyverno/policy-reporter-ui - pullPolicy: IfNotPresent - tag: 1.9.2 - -# sidecarContainers - add more containers to Kyverno ui -# Key/Value where Key is the sidecar `- name: ` -# Example: -# for adding OAuth authentication to Kyverno ui -# sidecarContainers: -# oauth-proxy: -# image: quay.io/oauth2-proxy/oauth2-proxy:v7.6.0 -# args: -# - --upstream=http://127.0.0.1:8080 -# - --http-address=0.0.0.0:8081 -# - ... -# ports: -# - containerPort: 8081 -# name: oauth-proxy -# protocol: TCP -# resources: {} -sidecarContainers: {} - -# possible default displayModes: light/dark -displayMode: "" - -# default refreshInterval, set 0 to disable it -refreshInterval: 10000 - -# Key/value pairs that are attached to all resources. -annotations: {} - -log: - # holds the latest 200 validation results in the UI Log - size: 200 - -# enable/disable views as needed in the Policy Reporter UI -# disabled log view will also disable the UI as push target -views: - dashboard: - policyReports: true - clusterPolicyReports: true - logs: true - policyReports: true - clusterPolicyReports: true - kyvernoPolicies: true - kyvernoVerifyImages: true - -plugins: - kyverno: false - -# Custom Cluster Name which is used in the ClusterSelect, if you configured additional clusters below. -clusterName: "" - -# Attention: be sure that your APIs are not accessable for the outside world -# Use tools like VPN, private Networks or internal Network Load Balancer to expose your APIs in a secure way to the UI -clusters: [] -# - name: External Cluster -# api: https://policy-reporter.external.cluster # reachable external Policy Reporter REST API -# kyvernoApi: https://policy-reporter-kyverno-plugin.external.cluster # (optional) reachable external Policy Reporter Kyverno Plugin REST API -# skipTLS: false -# certificate: "/app/certs/root.ca" -# secreRef: "" # name of an existing secret to read the clusterconfiguration from, supported keys: api, kyvernoApi, username, password, skipTLS, certificate -# basicAuth: # added as HTTP BasicAuthentication Header for all requests against api and kyvernoApi -# username: "" -# password: "" - -# define custom filter for policy report results based on (Cluster)PolicyReport labels -# exmaple - use a owner label on all reports belonging to a dedicated team and add the label as additional custom filter -# -# apiVersion: wgpolicyk8s.io/v1alpha2 -# kind: PolicyReport -# metadata: -# labels: -# app.kubernetes.io/managed-by: kyverno -# owner: team-a -# name: cpol-disallow-capabilities -# namespace: default -# results: [...] -# -# labelFilter: ["owner"] -labelFilter: [] - -# Proxy request logging -logging: - encoding: console # possible encodings are console and json - logLevel: 0 # default info - development: false # more human readable structure, removes log sampling - -api: - logging: false # enables access logging for proxy requests, sets log level to debug - overwriteHost: true # overwrites request host and sets X-Forwarded--Host and X-Origin-Host headers - -# use redis as external log storage instead of an in memory store -# recommended when using a HA setup with more then one replica -# to get all logs on each instance -redis: - enabled: false - address: "" - database: 0 - prefix: "policy-reporter-ui" - username: "" - password: "" - -# configurations related to the PolicyReporter API -policyReporter: - port: 8080 - -# configurations related to the RolicyReporter KyvernoPlugin API -kyvernoPlugin: - port: 8080 - -# configure additional volumes to e.g. mount custom certificate for proxy TLS -volumes: [] -volumeMounts: [] - -imagePullSecrets: [] - -replicaCount: 1 - -revisionHistoryLimit: 10 - -deploymentStrategy: {} - # rollingUpdate: - # maxSurge: 25% - # maxUnavailable: 25% - # type: RollingUpdate - -securityContext: - runAsUser: 1234 - runAsNonRoot: true - privileged: false - allowPrivilegeEscalation: false - readOnlyRootFilesystem: true - capabilities: - drop: - - ALL - seccompProfile: - type: RuntimeDefault - -# Key/value pairs that are attached to pods. -podAnnotations: {} - -# Key/value pairs that are attached to pods. -podLabels: {} - -# Allow additional env variables to be added -envVars: [] - -resources: {} - # We usually recommend not to specify default resources and to leave this as a conscious - # choice for the user. This also increases chances charts run on environments with little - # resources, such as Minikube. If you do want to specify resources, uncomment the following - # lines, adjust them as necessary, and remove the curly braces after 'resources:'. - # limits: - # memory: 100Mi - # cpu: 50m - # requests: - # memory: 50Mi - # cpu: 10m - -serviceAccount: - # Specifies whether a service account should be created - create: true - # Annotations to add to the service account - annotations: {} - # The name of the service account to use. - # If not set and create is true, a name is generated using the fullname template - name: "" - -# Create secret reader role and rolebinding -rbac: - enabled: true - -service: - enabled: true - ## configuration of service - # key/value - annotations: {} - # key/value - labels: {} - type: ClusterIP - # integer nubmer. This is port for service - port: 8080 - # additionalPorts: - # - name: authenticated - # port: 8081 - # targetPort: 8081 - additionalPorts: [] - -# enabled if replicaCount > 1 -podDisruptionBudget: - # -- Configures the minimum available pods for policy-reporter-ui disruptions. - # Cannot be used if `maxUnavailable` is set. - minAvailable: 1 - # -- Configures the maximum unavailable pods for policy-reporter-ui disruptions. - # Cannot be used if `minAvailable` is set. - maxUnavailable: - -## Set to true to enable ingress record generation -# ref to: https://kubernetes.io/docs/concepts/services-networking/ingress/ -ingress: - enabled: false - className: "" - # key/value - labels: {} - # key/value - annotations: {} - # kubernetes.io/ingress.class: nginx - # kubernetes.io/tls-acme: "true" - ## Redirect ingress to an additional defined port on the service - # port: 8081 - hosts: - - host: chart-example.local - paths: [] - tls: [] - # - secretName: chart-example-tls - # hosts: - # - chart-example.local - -# Node labels for pod assignment -# ref: https://kubernetes.io/docs/user-guide/node-selection/ -nodeSelector: {} - -# Tolerations for pod assignment -# ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ -tolerations: [] - -# Anti-affinity to disallow deploying client and master nodes on the same worker node -affinity: {} - -# Topology Spread Constraints to better spread pods -topologySpreadConstraints: [] - -# enable a NetworkPolicy for this chart. Useful on clusters where Network Policies are -# used and configured in a default-deny fashion. -networkPolicy: - enabled: false - egress: [] - -# Should be set in the parent chart only -global: - # available plugins - plugins: - # enable kyverno for Policy Reporter UI and monitoring - kyverno: false - # overwrite the fullname of all resources including subcharts - fullnameOverride: "" - # configure the namespace of all resources including subcharts - namespace: "" - # additional labels added on each resource - labels: {} - # basicAuth for APIs and metrics - basicAuth: - # HTTP BasicAuth username - username: "" - # HTTP BasicAuth password - password: "" - # read credentials from secret - secretRef: "" - -# Extra manifests to deploy as an array -extraManifests: [] - # - | - # apiVersion: v1 - # kind: ConfigMap - # metadata: - # labels: - # name: kyverno-extra - # data: - # extra-data: "value" \ No newline at end of file diff --git a/charts/policy-reporter/config.yaml b/charts/policy-reporter/config.yaml deleted file mode 100644 index 04cae54c..00000000 --- a/charts/policy-reporter/config.yaml +++ /dev/null @@ -1,416 +0,0 @@ -loki: - host: {{ .Values.target.loki.host | quote }} - certificate: {{ .Values.target.loki.certificate | quote }} - skipTLS: {{ .Values.target.loki.skipTLS }} - path: {{ .Values.target.loki.path | quote }} - secretRef: {{ .Values.target.loki.secretRef | quote }} - mountedSecret: {{ .Values.target.loki.mountedSecret | quote }} - minimumPriority: {{ .Values.target.loki.minimumPriority | quote }} - skipExistingOnStartup: {{ .Values.target.loki.skipExistingOnStartup }} - username: {{ .Values.target.loki.username | quote }} - password: {{ .Values.target.loki.password | quote }} - {{- with .Values.target.loki.customLabels }} - customLabels: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.loki.sources }} - sources: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.loki.filter }} - filter: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.loki.headers }} - headers: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.loki.channels }} - channels: - {{- toYaml . | nindent 4 }} - {{- end }} - -elasticsearch: - host: {{ .Values.target.elasticsearch.host | quote }} - certificate: {{ .Values.target.elasticsearch.certificate | quote }} - skipTLS: {{ .Values.target.elasticsearch.skipTLS }} - username: {{ .Values.target.elasticsearch.username | quote }} - password: {{ .Values.target.elasticsearch.password | quote }} - apiKey: {{ .Values.target.elasticsearch.apiKey | quote }} - secretRef: {{ .Values.target.elasticsearch.secretRef | quote }} - mountedSecret: {{ .Values.target.elasticsearch.mountedSecret | quote }} - index: {{ .Values.target.elasticsearch.index | default "policy-reporter" | quote }} - rotation: {{ .Values.target.elasticsearch.rotation | default "daily" | quote }} - minimumPriority: {{ .Values.target.elasticsearch.minimumPriority | quote }} - skipExistingOnStartup: {{ .Values.target.elasticsearch.skipExistingOnStartup }} - typelessApi: {{ .Values.target.elasticsearch.typelessApi }} - {{- with .Values.target.elasticsearch.sources }} - sources: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.elasticsearch.customFields }} - customFields: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.elasticsearch.filter }} - filter: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.elasticsearch.channels }} - channels: - {{- toYaml . | nindent 4 }} - {{- end }} - -slack: - webhook: {{ .Values.target.slack.webhook | quote }} - channel: {{ .Values.target.slack.channel | quote }} - secretRef: {{ .Values.target.slack.secretRef | quote }} - mountedSecret: {{ .Values.target.slack.mountedSecret | quote }} - minimumPriority: {{ .Values.target.slack.minimumPriority | quote }} - skipExistingOnStartup: {{ .Values.target.slack.skipExistingOnStartup }} - {{- with .Values.target.slack.customFields }} - customFields: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.slack.sources }} - sources: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.slack.filter }} - filter: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.slack.channels }} - channels: - {{- toYaml . | nindent 4 }} - {{- end }} - -discord: - webhook: {{ .Values.target.discord.webhook | quote }} - secretRef: {{ .Values.target.discord.secretRef | quote }} - mountedSecret: {{ .Values.target.discord.mountedSecret | quote }} - minimumPriority: {{ .Values.target.discord.minimumPriority | quote }} - skipExistingOnStartup: {{ .Values.target.discord.skipExistingOnStartup }} - {{- with .Values.target.discord.customFields }} - customFields: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.discord.sources }} - sources: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.discord.filter }} - filter: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.discord.channels }} - channels: - {{- toYaml . | nindent 4 }} - {{- end }} - -teams: - webhook: {{ .Values.target.teams.webhook | quote }} - certificate: {{ .Values.target.teams.certificate | quote }} - skipTLS: {{ .Values.target.teams.skipTLS }} - secretRef: {{ .Values.target.teams.secretRef | quote }} - mountedSecret: {{ .Values.target.teams.mountedSecret | quote }} - minimumPriority: {{ .Values.target.teams.minimumPriority | quote }} - skipExistingOnStartup: {{ .Values.target.teams.skipExistingOnStartup }} - {{- with .Values.target.teams.customFields }} - customFields: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.teams.sources }} - sources: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.teams.filter }} - filter: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.teams.channels }} - channels: - {{- toYaml . | nindent 4 }} - {{- end }} - -webhook: - host: {{ .Values.target.webhook.host | quote }} - certificate: {{ .Values.target.webhook.certificate | quote }} - skipTLS: {{ .Values.target.webhook.skipTLS }} - secretRef: {{ .Values.target.webhook.secretRef | quote }} - mountedSecret: {{ .Values.target.webhook.mountedSecret | quote }} - minimumPriority: {{ .Values.target.webhook.minimumPriority | quote }} - skipExistingOnStartup: {{ .Values.target.webhook.skipExistingOnStartup }} - {{- with .Values.target.webhook.headers }} - headers: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.webhook.sources }} - sources: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.webhook.customFields }} - customFields: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.webhook.filter }} - filter: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.webhook.channels }} - channels: - {{- toYaml . | nindent 4 }} - {{- end }} - -telegram: - token: {{ .Values.target.telegram.token | quote }} - chatID: {{ .Values.target.telegram.chatID | quote }} - host: {{ .Values.target.telegram.host | quote }} - certificate: {{ .Values.target.telegram.certificate | quote }} - skipTLS: {{ .Values.target.telegram.skipTLS }} - secretRef: {{ .Values.target.telegram.secretRef | quote }} - mountedSecret: {{ .Values.target.telegram.mountedSecret | quote }} - minimumPriority: {{ .Values.target.telegram.minimumPriority | quote }} - skipExistingOnStartup: {{ .Values.target.telegram.skipExistingOnStartup }} - {{- with .Values.target.telegram.headers }} - headers: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.telegram.sources }} - sources: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.telegram.customFields }} - customFields: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.telegram.filter }} - filter: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.telegram.channels }} - channels: - {{- toYaml . | nindent 4 }} - {{- end }} - -googleChat: - webhook: {{ .Values.target.googleChat.webhook | quote }} - certificate: {{ .Values.target.googleChat.certificate | quote }} - skipTLS: {{ .Values.target.googleChat.skipTLS }} - secretRef: {{ .Values.target.googleChat.secretRef | quote }} - mountedSecret: {{ .Values.target.googleChat.mountedSecret | quote }} - minimumPriority: {{ .Values.target.googleChat.minimumPriority | quote }} - skipExistingOnStartup: {{ .Values.target.googleChat.skipExistingOnStartup }} - {{- with .Values.target.googleChat.headers }} - headers: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.googleChat.sources }} - sources: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.googleChat.customFields }} - customFields: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.googleChat.filter }} - filter: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.googleChat.channels }} - channels: - {{- toYaml . | nindent 4 }} - {{- end }} - -ui: - host: {{ include "policyreporter.uihost" . }} - certificate: {{ .Values.target.ui.certificate | quote }} - skipTLS: {{ .Values.target.ui.skipTLS }} - minimumPriority: {{ .Values.target.ui.minimumPriority | quote }} - skipExistingOnStartup: {{ .Values.target.ui.skipExistingOnStartup }} - {{- with .Values.target.ui.sources }} - sources: - {{- toYaml . | nindent 4 }} - {{- end }} - -s3: - accessKeyID: {{ .Values.target.s3.accessKeyID }} - secretAccessKey: {{ .Values.target.s3.secretAccessKey }} - secretRef: {{ .Values.target.s3.secretRef | quote }} - mountedSecret: {{ .Values.target.s3.mountedSecret }} - region: {{ .Values.target.s3.region }} - endpoint: {{ .Values.target.s3.endpoint }} - bucket: {{ .Values.target.s3.bucket }} - bucketKeyEnabled: {{ .Values.target.s3.bucketKeyEnabled }} - kmsKeyId: {{ .Values.target.s3.kmsKeyId }} - serverSideEncryption: {{ .Values.target.s3.serverSideEncryption }} - pathStyle: {{ .Values.target.s3.pathStyle }} - prefix: {{ .Values.target.s3.prefix }} - minimumPriority: {{ .Values.target.s3.minimumPriority | quote }} - skipExistingOnStartup: {{ .Values.target.s3.skipExistingOnStartup }} - {{- with .Values.target.s3.sources }} - sources: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.s3.customFields }} - customFields: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.s3.filter }} - filter: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.s3.channels }} - channels: - {{- toYaml . | nindent 4 }} - {{- end }} - -kinesis: - accessKeyID: {{ .Values.target.kinesis.accessKeyID }} - secretAccessKey: {{ .Values.target.kinesis.secretAccessKey }} - secretRef: {{ .Values.target.kinesis.secretRef | quote }} - mountedSecret: {{ .Values.target.kinesis.mountedSecret | quote }} - region: {{ .Values.target.kinesis.region }} - endpoint: {{ .Values.target.kinesis.endpoint }} - streamName: {{ .Values.target.kinesis.streamName }} - minimumPriority: {{ .Values.target.kinesis.minimumPriority | quote }} - skipExistingOnStartup: {{ .Values.target.kinesis.skipExistingOnStartup }} - {{- with .Values.target.kinesis.sources }} - sources: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.kinesis.customFields }} - customFields: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.kinesis.filter }} - filter: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.kinesis.channels }} - channels: - {{- toYaml . | nindent 4 }} - {{- end }} - -securityHub: - accountID: {{ .Values.target.securityHub.accountID }} - accessKeyID: {{ .Values.target.securityHub.accessKeyID }} - secretAccessKey: {{ .Values.target.securityHub.secretAccessKey }} - delayInSeconds: {{ .Values.target.securityHub.delayInSeconds }} - cleanup: {{ .Values.target.securityHub.cleanup }} - secretRef: {{ .Values.target.securityHub.secretRef | quote }} - mountedSecret: {{ .Values.target.securityHub.mountedSecret | quote }} - productName: {{ .Values.target.securityHub.productName | quote }} - companyName: {{ .Values.target.securityHub.companyName | quote }} - region: {{ .Values.target.securityHub.region }} - endpoint: {{ .Values.target.securityHub.endpoint }} - minimumPriority: {{ .Values.target.securityHub.minimumPriority | quote }} - skipExistingOnStartup: {{ .Values.target.securityHub.skipExistingOnStartup }} - {{- with .Values.target.securityHub.sources }} - sources: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.securityHub.customFields }} - customFields: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.securityHub.filter }} - filter: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.securityHub.channels }} - channels: - {{- toYaml . | nindent 4 }} - {{- end }} - -gcs: - credentials: {{ .Values.target.gcs.credentials }} - secretRef: {{ .Values.target.gcs.secretRef | quote }} - mountedSecret: {{ .Values.target.gcs.mountedSecret | quote }} - bucket: {{ .Values.target.gcs.bucket }} - prefix: {{ .Values.target.gcs.prefix }} - minimumPriority: {{ .Values.target.gcs.minimumPriority | quote }} - skipExistingOnStartup: {{ .Values.target.gcs.skipExistingOnStartup }} - {{- with .Values.target.gcs.sources }} - sources: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.gcs.customFields }} - customFields: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.gcs.filter }} - filter: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.target.gcs.channels }} - channels: - {{- toYaml . | nindent 4 }} - {{- end }} - -worker: {{ .Values.worker }} - -metrics: - mode: {{ .Values.metrics.mode }} - {{- with .Values.metrics.filter }} - filter: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.metrics.customLabels }} - customLabels: - {{- toYaml . | nindent 4 }} - {{- end }} - -reportFilter: - namespaces: - {{- with .Values.reportFilter.namespaces.include }} - include: - {{- toYaml . | nindent 6 }} - {{- end }} - {{- with .Values.reportFilter.namespaces.exclude }} - exclude: - {{- toYaml . | nindent 6 }} - {{- end }} - clusterReports: - disabled: {{ .Values.reportFilter.clusterReports.disabled }} - -leaderElection: - enabled: {{ or .Values.leaderElection.enabled (gt (int .Values.replicaCount) 1) }} - releaseOnCancel: {{ .Values.leaderElection.releaseOnCancel }} - leaseDuration: {{ .Values.leaderElection.leaseDuration }} - renewDeadline: {{ .Values.leaderElection.renewDeadline }} - retryPeriod: {{ .Values.leaderElection.retryPeriod }} - -{{- with .Values.redis }} -redis: - {{- toYaml . | nindent 2 }} -{{- end }} - -{{- with .Values.sourceConfig }} -sourceConfig: - {{- toYaml . | nindent 2 }} -{{- end }} - - -logging: - encoding: {{ .Values.logging.encoding }} - logLevel: {{ include "policyreporter.logLevel" . }} - development: {{ .Values.logging.development }} - -api: - logging: {{ .Values.api.logging }} - basicAuth: - username: {{ .Values.global.basicAuth.username }} - password: {{ .Values.global.basicAuth.password }} - secretRef: {{ .Values.global.basicAuth.secretRef }} - -database: - type: {{ .Values.database.type }} - database: {{ .Values.database.database }} - username: {{ .Values.database.username }} - password: {{ .Values.database.password }} - host: {{ .Values.database.host }} - enableSSL: {{ .Values.database.enableSSL }} - dsn: {{ .Values.database.dsn }} - secretRef: {{ .Values.database.secretRef }} - mountedSecret: {{ .Values.database.mountedSecret }} diff --git a/charts/policy-reporter/configs/core.tmpl b/charts/policy-reporter/configs/core.tmpl new file mode 100644 index 00000000..2b5f111c --- /dev/null +++ b/charts/policy-reporter/configs/core.tmpl @@ -0,0 +1,176 @@ +target: + loki: + {{- include "target.loki" .Values.target.loki | nindent 4 }} + {{- if and .Values.target.loki .Values.target.loki.channels }} + channels: + {{- range .Values.target.loki.channels }} + - + {{- include "target.loki" . | nindent 8 }} + {{- end }} + {{- end }} + + elasticsearch: + {{- include "target.elasticsearch" .Values.target.elasticsearch | nindent 4 }} + {{- if and .Values.target.elasticsearch .Values.target.elasticsearch.channels }} + channels: + {{- range .Values.target.elasticsearch.channels }} + - + {{- include "target.elasticsearch" . | nindent 8 }} + {{- end }} + {{- end }} + + slack: + {{- include "target.slack" .Values.target.slack | nindent 4 }} + {{- if and .Values.target.slack .Values.target.slack.channels }} + channels: + {{- range .Values.target.slack.channels }} + - + {{- include "target.slack" . | nindent 8 }} + {{- end }} + {{- end }} + + discord: + {{- include "target.webhook" .Values.target.discord | nindent 4 }} + {{- if and .Values.target.discord .Values.target.discord.channels }} + channels: + {{- range .Values.target.discord.channels }} + - + {{- include "target.webhook" . | nindent 8 }} + {{- end }} + {{- end }} + + teams: + {{- include "target.webhook" .Values.target.teams | nindent 4 }} + {{- if and .Values.target.teams .Values.target.teams.channels }} + channels: + {{- range .Values.target.teams.channels }} + - + {{- include "target.webhook" . | nindent 8 }} + {{- end }} + {{- end }} + + webhook: + {{- include "target.webhook" .Values.target.webhook | nindent 4 }} + {{- if and .Values.target.webhook .Values.target.webhook.channels }} + channels: + {{- range .Values.target.webhook.channels }} + - + {{- include "target.webhook" . | nindent 8 }} + {{- end }} + {{- end }} + + telegram: + {{- include "target.telegram" .Values.target.telegram | nindent 4 }} + {{- if and .Values.target.telegram .Values.target.telegram.channels }} + channels: + {{- range .Values.target.telegram.channels }} + - + {{- include "target.telegram" . | nindent 8 }} + {{- end }} + {{- end }} + + googleChat: + {{- include "target.webhook" .Values.target.googleChat | nindent 4 }} + {{- if and .Values.target.webhook .Values.target.googleChat.channels }} + channels: + {{- range .Values.target.googleChat.channels }} + - + {{- include "target.webhook" . | nindent 8 }} + {{- end }} + {{- end }} + + s3: + {{- include "target.s3" .Values.target.s3 | nindent 4 }} + {{- if and .Values.target.s3 .Values.target.s3.channels }} + channels: + {{- range .Values.target.s3.channels }} + - + {{- include "target.s3" . | nindent 8 }} + {{- end }} + {{- end }} + + kinesis: + {{- include "target.kinesis" .Values.target.kinesis | nindent 4 }} + {{- if and .Values.target.kinesis .Values.target.kinesis.channels }} + channels: + {{- range .Values.target.kinesis.channels }} + - + {{- include "target.kinesis" . | nindent 8 }} + {{- end }} + {{- end }} + + securityHub: + {{- include "target.securityhub" .Values.target.securityHub | nindent 4 }} + {{- if and .Values.target.securityHub .Values.target.securityHub.channels }} + channels: + {{- range .Values.target.securityHub.channels }} + - + {{- include "target.securityhub" . | nindent 8 }} + {{- end }} + {{- end }} + + gcs: + {{- include "target.gcs" .Values.target.gcs | nindent 4 }} + {{- if and .Values.target.gcs .Values.target.gcs.channels }} + channels: + {{- range .Values.target.gcs.channels }} + - + {{- include "target.gcs" . | nindent 8 }} + {{- end }} + {{- end }} + +worker: {{ .Values.worker }} + +{{- with .Values.metrics }} +metrics: + {{- toYaml . | nindent 2 }} +{{- end }} + +{{- with .Values.reportFilter }} +reportFilter: + {{- toYaml . | nindent 2 }} +{{- end }} + +{{- with .Values.sourceFilters }} +sourceFilters: + {{- toYaml . | nindent 2 }} +{{- end }} + +leaderElection: + enabled: {{ gt (int .Values.replicaCount) 1 }} + releaseOnCancel: {{ .Values.leaderElection.releaseOnCancel }} + leaseDuration: {{ .Values.leaderElection.leaseDuration }} + renewDeadline: {{ .Values.leaderElection.renewDeadline }} + retryPeriod: {{ .Values.leaderElection.retryPeriod }} + +{{- with .Values.redis }} +redis: + {{- toYaml . | nindent 2 }} +{{- end }} + +{{- with .Values.sourceConfig }} +sourceConfig: + {{- toYaml . | nindent 2 }} +{{- end }} + +logging: + server: {{ .Values.logging.server }} + encoding: {{ .Values.logging.encoding }} + logLevel: {{ include "policyreporter.logLevel" . }} + +api: + basicAuth: + username: {{ .Values.basicAuth.username }} + password: {{ .Values.basicAuth.password }} + secretRef: {{ .Values.basicAuth.secretRef }} + +database: + type: {{ .Values.database.type }} + database: {{ .Values.database.database }} + username: {{ .Values.database.username }} + password: {{ .Values.database.password }} + host: {{ .Values.database.host }} + enableSSL: {{ .Values.database.enableSSL }} + dsn: {{ .Values.database.dsn }} + secretRef: {{ .Values.database.secretRef }} + mountedSecret: {{ .Values.database.mountedSecret }} diff --git a/charts/policy-reporter/config-email-reports.yaml b/charts/policy-reporter/configs/email-reports.tmpl similarity index 100% rename from charts/policy-reporter/config-email-reports.yaml rename to charts/policy-reporter/configs/email-reports.tmpl diff --git a/charts/policy-reporter/configs/kyverno-plugin.tmpl b/charts/policy-reporter/configs/kyverno-plugin.tmpl new file mode 100644 index 00000000..491a7925 --- /dev/null +++ b/charts/policy-reporter/configs/kyverno-plugin.tmpl @@ -0,0 +1,27 @@ +leaderElection: + enabled: {{ gt (int .Values.plugin.kyverno.replicaCount) 1 }} + releaseOnCancel: {{ .Values.plugin.kyverno.leaderElection.releaseOnCancel }} + leaseDuration: {{ .Values.plugin.kyverno.leaderElection.leaseDuration }} + renewDeadline: {{ .Values.plugin.kyverno.leaderElection.renewDeadline }} + retryPeriod: {{ .Values.plugin.kyverno.leaderElection.retryPeriod }} + lockName: {{ .Values.plugin.kyverno.leaderElection.lockName }} + +logging: + api: {{ .Values.plugin.kyverno.logging.api }} + server: {{ .Values.plugin.kyverno.logging.server }} + encoding: {{ .Values.plugin.kyverno.logging.encoding }} + logLevel: {{ .Values.plugin.kyverno.logging.logLevel }} + +server: + basicAuth: + username: {{ .Values.basicAuth.username }} + password: {{ .Values.basicAuth.password }} + secretRef: {{ .Values.basicAuth.secretRef }} + +core: + host: {{ printf "http://%s:%d" (include "policyreporter.fullname" .) (.Values.service.port | int) }} + +{{- with .Values.plugin.kyverno.blockReports }} +blockReports: + {{- toYaml . | nindent 4 }} +{{- end }} diff --git a/charts/policy-reporter/configs/trivy-plugin.tmpl b/charts/policy-reporter/configs/trivy-plugin.tmpl new file mode 100644 index 00000000..5ba89d39 --- /dev/null +++ b/charts/policy-reporter/configs/trivy-plugin.tmpl @@ -0,0 +1,20 @@ +logging: + api: {{ .Values.plugin.trivy.logging.api }} + server: {{ .Values.plugin.trivy.logging.server }} + encoding: {{ .Values.plugin.trivy.logging.encoding }} + logLevel: {{ .Values.plugin.trivy.logging.logLevel }} + +server: + basicAuth: + username: {{ .Values.basicAuth.username }} + password: {{ .Values.basicAuth.password }} + secretRef: {{ .Values.basicAuth.secretRef }} + +core: + host: {{ printf "http://%s:%d" (include "policyreporter.fullname" .) (.Values.service.port | int) }} + skipTLS: {{ .Values.plugin.trivy.policyReporter.skipTLS }} + certificate: {{ .Values.plugin.trivy.policyReporter.certificate }} + secretRef: {{ .Values.plugin.trivy.policyReporter.secretRef }} + basicAuth: + username: {{ .Values.basicAuth.username }} + password: {{ .Values.basicAuth.password }} diff --git a/charts/policy-reporter/configs/ui.tmpl b/charts/policy-reporter/configs/ui.tmpl new file mode 100644 index 00000000..8f5f3ef6 --- /dev/null +++ b/charts/policy-reporter/configs/ui.tmpl @@ -0,0 +1,70 @@ +namespace: {{ .Release.Namespace }} + +tempDir: {{ .Values.ui.tempDir }} + +logging: + api: {{ .Values.ui.logging.api }} + server: {{ .Values.ui.logging.server }} + encoding: {{ .Values.ui.logging.encoding }} + logLevel: {{ .Values.ui.logging.logLevel }} + +server: + port: {{ .Values.ui.server.port }} + cors: {{ .Values.ui.server.cors }} + overwriteHost: {{ .Values.ui.server.overwriteHost }} + +ui: + displayMode: {{ .Values.ui.displayMode }} + banner: {{ .Values.ui.banner }} + +{{- $default := false -}} +{{- range .Values.ui.clusters }} + {{- if eq .name .Values.ui.name -}} + {{- $default = true -}} + {{- end -}} +{{- end }} + +clusters: +{{- if not $default }} + - name: {{ .Values.ui.name }} + secretRef: {{ include "ui.fullname" . }}-default-cluster +{{- end }} +{{- with .Values.ui.clusters }} + {{- toYaml . | nindent 2 }} +{{- end }} + +{{- with .Values.ui.customBoards }} +customBoards: + {{- toYaml . | nindent 2 }} +{{- end }} + +{{- $kyverno := false -}} +{{- range .Values.ui.sources }} + {{- if eq .name "kyverno" -}} + {{- $kyverno = true -}} + {{- end -}} +{{- end }} + +sources: +{{- if not $kyverno }} + - name: kyverno + chartType: result + exceptions: false + excludes: + results: + - warn + - error +{{- end }} +{{- with .Values.ui.sources }} + {{- toYaml . | nindent 2 }} +{{- end }} + +{{- with .Values.ui.openIDConnect }} +openIDConnect: + {{- toYaml . | nindent 4 }} +{{- end }} + +{{- with .Values.ui.oauth }} +oauth: + {{- toYaml . | nindent 4 }} +{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/templates/_helpers.tpl b/charts/policy-reporter/templates/_helpers.tpl index 644d1935..ea498ee5 100644 --- a/charts/policy-reporter/templates/_helpers.tpl +++ b/charts/policy-reporter/templates/_helpers.tpl @@ -9,8 +9,8 @@ If release name contains chart name it will be used as a full name. */}} {{- define "policyreporter.fullname" -}} {{- $name := default .Chart.Name .Values.nameOverride }} -{{- if .Values.global.fullnameOverride }} -{{- .Values.global.fullnameOverride }} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride }} {{- else if contains $name .Release.Name }} {{- .Release.Name | trunc 63 | trimSuffix "-" }} {{- else }} @@ -78,8 +78,6 @@ Create UI target host based on configuration {{- .Values.target.ui.host }} {{- else if not .Values.ui.enabled }} {{- "" }} -{{- else if and .Values.ui.enabled (and .Values.ui.views.logs .Values.ui.service.enabled) }} -{{- printf "http://%s:%s" (include "ui.fullname" .) (.Values.ui.service.port | toString) }} {{- else }} {{- "" }} {{- end }} @@ -95,7 +93,7 @@ Create UI target host based on configuration {{- define "policyreporter.podDisruptionBudget" -}} {{- if and .Values.podDisruptionBudget.minAvailable .Values.podDisruptionBudget.maxUnavailable }} -{{- fail "Cannot set both .Values.podDisruptionBudget.minAvailable and .Values.podDisruptionBudget.maxUnavailable" -}} +{{- fail "Cannot set both minAvailable and maxUnavailable" -}} {{- end }} {{- if not .Values.podDisruptionBudget.maxUnavailable }} minAvailable: {{ default 1 .Values.podDisruptionBudget.minAvailable }} @@ -107,8 +105,8 @@ maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }} {{/* Get the namespace name. */}} {{- define "policyreporter.namespace" -}} -{{- if .Values.global.namespace -}} - {{- .Values.global.namespace -}} +{{- if .Values.namespaceOverride -}} + {{- .Values.namespaceOverride -}} {{- else -}} {{- .Release.Namespace -}} {{- end -}} @@ -116,9 +114,137 @@ maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }} {{/* Get the namespace name. */}} {{- define "policyreporter.logLevel" -}} -{{- if .Values.api.logging -}} +{{- if .Values.logging.server -}} -1 {{- else -}} {{- .Values.logging.logLevel -}} {{- end -}} {{- end -}} + +{{- define "target" -}} +name: {{ .name | quote }} +secretRef: {{ .secretRef | quote }} +mountedSecret: {{ .mountedSecret | quote }} +minimumSeverity: {{ .minimumSeverity | quote }} +skipExistingOnStartup: {{ .skipExistingOnStartup }} +{{- with .customFields }} +customFields: +{{- toYaml . | nindent 2 }} +{{- end }} +{{- with .sources }} +sources: +{{- toYaml . | nindent 2 }} +{{- end }} +{{- with .filter }} +filter: +{{- toYaml . | nindent 2 }} +{{- end }} +{{- end }} + +{{- define "target.loki" -}} +config: + host: {{ .host | quote }} + certificate: {{ .certificate | quote }} + skipTLS: {{ .skipTLS }} + path: {{ .path | quote }} +{{ include "target" . }} +{{- end }} + +{{- define "target.elasticsearch" -}} +config: + host: {{ .host | quote }} + certificate: {{ .certificate | quote }} + skipTLS: {{ .skipTLS }} + username: {{ .username | quote }} + password: {{ .password | quote }} + apiKey: {{ .apiKey | quote }} + index: {{ .index| quote }} + rotation: {{ .rotation | quote }} +{{ include "target" . }} +{{- end }} + +{{- define "target.slack" -}} +config: + webhook: {{ .webhook | quote }} + channel: {{ .channel | quote }} + certificate: {{ .certificate | quote }} + skipTLS: {{ .skipTLS }} + {{- with .headers }} + headers: + {{- toYaml . | nindent 4 }} + {{- end }} +{{ include "target" . }} +{{- end }} + +{{- define "target.webhook" -}} +config: + webhook: {{ .webhook | quote }} + certificate: {{ .certificate | quote }} + skipTLS: {{ .skipTLS }} + {{- with .headers }} + headers: + {{- toYaml . | nindent 4 }} + {{- end }} +{{ include "target" . }} +{{- end }} + +{{- define "target.telegram" -}} +config: + chatId: {{ .chatId | quote }} + token: {{ .token | quote }} + webhook: {{ .webhook | quote }} + certificate: {{ .certificate | quote }} + skipTLS: {{ .skipTLS }} + {{- with .headers }} + headers: + {{- toYaml . | nindent 4 }} + {{- end }} +{{ include "target" . }} +{{- end }} + +{{- define "target.s3" -}} +config: + accessKeyId: {{ .accessKeyId }} + secretAccessKey: {{ .secretAccessKey }} + region: {{ .region }} + endpoint: {{ .endpoint }} + bucket: {{ .bucket }} + bucketKeyEnabled: {{ .bucketKeyEnabled }} + kmsKeyId: {{ .kmsKeyId }} + serverSideEncryption: {{ .serverSideEncryption }} + pathStyle: {{ .pathStyle }} + prefix: {{ .prefix }} +{{ include "target" . }} +{{- end }} + +{{- define "target.kinesis" -}} +config: + accessKeyId: {{ .accessKeyId }} + secretAccessKey: {{ .secretAccessKey }} + region: {{ .region }} + endpoint: {{ .endpoint }} + streamName: {{ .streamName }} +{{ include "target" . }} +{{- end }} + +{{- define "target.securityhub" -}} +config: + accessKeyId: {{ .accessKeyId }} + secretAccessKey: {{ .secretAccessKey }} + region: {{ .region }} + endpoint: {{ .endpoint }} + accountId: {{ .accountId }} + productName: {{ .productName }} + companyName: {{ .companyName }} + delayInSeconds: {{ .delayInSeconds }} + synchronize: {{ .synchronize }} +{{ include "target" . }} +{{- end }} + +{{- define "target.gcs" -}} +config: + credentials: {{ .credentials }} + bucket: {{ .bucket }} + prefix: {{ .prefix }} +{{ include "target" . }} +{{- end }} diff --git a/charts/policy-reporter/templates/cluster-secret.yaml b/charts/policy-reporter/templates/cluster-secret.yaml new file mode 100644 index 00000000..7ca7c347 --- /dev/null +++ b/charts/policy-reporter/templates/cluster-secret.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "ui.fullname" . }}-default-cluster + namespace: {{ include "policyreporter.namespace" . }} + {{- if .Values.annotations }} + annotations: + {{- toYaml .Values.annotations | nindent 4 }} + {{- end }} + labels: + {{- include "policyreporter.labels" . | nindent 4 }} +type: Opaque +data: + {{- $username := .Values.basicAuth.username }} + {{- $password := .Values.basicAuth.password }} + host: {{ printf "http://%s:%d" (include "policyreporter.fullname" .) (.Values.service.port | int) | b64enc }} + {{- if .Values.plugin.kyverno.enabled }} + {{- $host := printf "http://%s:%d" (include "kyverno-plugin.fullname" .) (.Values.plugin.kyverno.service.port | int) }} + plugin.kyverno: {{ (printf "{\"host\":\"%s\", \"name\":\"kyverno\", \"username\":\"%s\", \"password\":\"%s\"}" $host $username $password) | b64enc }} + {{- end }} + {{- if .Values.plugin.trivy.enabled }} + {{- $host := printf "http://%s:%d/vulnr" (include "trivy-plugin.fullname" .) (.Values.plugin.trivy.service.port | int) }} + plugin.trivy: {{ (printf "{\"host\":\"%s\", \"name\":\"Trivy Vulnerability\", \"username\":\"%s\", \"password\":\"%s\"}" $host $username $password) | b64enc }} + username: {{ $username | b64enc }} + password: {{ $password | b64enc }} + {{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/templates/clusterrole.yaml b/charts/policy-reporter/templates/clusterrole.yaml index d0505c41..41c5f56d 100644 --- a/charts/policy-reporter/templates/clusterrole.yaml +++ b/charts/policy-reporter/templates/clusterrole.yaml @@ -22,4 +22,22 @@ rules: - get - list - watch +- apiGroups: + - '' + resources: + - namespaces + verbs: + - list +- apiGroups: + - '' + resources: + - pods + verbs: + - get +- apiGroups: + - 'batch' + resources: + - jobs + verbs: + - get {{- end -}} diff --git a/charts/policy-reporter/templates/config-email-reports-secret.yaml b/charts/policy-reporter/templates/config-email-reports-secret.yaml index e08555d4..9fd7947f 100644 --- a/charts/policy-reporter/templates/config-email-reports-secret.yaml +++ b/charts/policy-reporter/templates/config-email-reports-secret.yaml @@ -12,5 +12,5 @@ metadata: {{- include "policyreporter.labels" . | nindent 4 }} type: Opaque data: - config.yaml: {{ tpl (.Files.Get "config-email-reports.yaml") . | b64enc }} + config.yaml: {{ tpl (.Files.Get "configs/email-reports.tmpl") . | b64enc }} {{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/templates/config-secret.yaml b/charts/policy-reporter/templates/config-secret.yaml index 88c7b614..37a97929 100644 --- a/charts/policy-reporter/templates/config-secret.yaml +++ b/charts/policy-reporter/templates/config-secret.yaml @@ -12,5 +12,5 @@ metadata: {{- include "policyreporter.labels" . | nindent 4 }} type: Opaque data: - config.yaml: {{ tpl (.Files.Get "config.yaml") . | b64enc }} -{{- end }} \ No newline at end of file + config.yaml: {{ tpl (.Files.Get "configs/core.tmpl") . | b64enc }} +{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/templates/cronjob-summary-report.yaml b/charts/policy-reporter/templates/cronjob-summary-report.yaml index 93d67f9c..cfec66e6 100644 --- a/charts/policy-reporter/templates/cronjob-summary-report.yaml +++ b/charts/policy-reporter/templates/cronjob-summary-report.yaml @@ -51,7 +51,7 @@ spec: {{- end }} restartPolicy: {{ .Values.emailReports.summary.restartPolicy }} containers: - - name: {{ default .Chart.Name .Values.nameOverride }} + - name: policy-reporter image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} {{- if .Values.securityContext }} diff --git a/charts/policy-reporter/templates/cronjob-violations-report.yaml b/charts/policy-reporter/templates/cronjob-violations-report.yaml index 9288365d..656d0c85 100644 --- a/charts/policy-reporter/templates/cronjob-violations-report.yaml +++ b/charts/policy-reporter/templates/cronjob-violations-report.yaml @@ -51,7 +51,7 @@ spec: {{- end }} restartPolicy: {{ .Values.emailReports.violations.restartPolicy }} containers: - - name: {{ default .Chart.Name .Values.nameOverride }} + - name: policy-reporter image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} {{- if .Values.securityContext }} diff --git a/charts/policy-reporter/templates/deployment.yaml b/charts/policy-reporter/templates/deployment.yaml index 4dd740ab..6351d865 100644 --- a/charts/policy-reporter/templates/deployment.yaml +++ b/charts/policy-reporter/templates/deployment.yaml @@ -12,9 +12,9 @@ metadata: spec: replicas: {{ .Values.replicaCount }} revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} - {{- if .Values.deploymentStrategy }} + {{- with .Values.plugin.kyverno.updateStrategy }} strategy: - {{- toYaml .Values.deploymentStrategy | nindent 4 }} + {{- toYaml . | nindent 4 }} {{- end }} selector: matchLabels: @@ -53,11 +53,11 @@ spec: {{- toYaml .Values.podSecurityContext | nindent 8 }} {{- end }} containers: - - name: {{ default .Chart.Name .Values.nameOverride }} + - name: policy-reporter image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} {{- if .Values.securityContext }} - securityContext: {{ include "policyreporter.securityContext" . | nindent 12 }} + securityContext: {{- include "policyreporter.securityContext" . | nindent 12 }} {{- end }} args: - --port={{ .Values.port.number }} @@ -99,7 +99,7 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace - {{- if or .Values.leaderElection.enabled (gt (int .Values.replicaCount) 1) }} + {{- if gt (int .Values.replicaCount) 1 }} - name: POD_NAME valueFrom: fieldRef: diff --git a/charts/policy-reporter/templates/monitoring/_helpers.tpl b/charts/policy-reporter/templates/monitoring/_helpers.tpl new file mode 100644 index 00000000..3f4c63b6 --- /dev/null +++ b/charts/policy-reporter/templates/monitoring/_helpers.tpl @@ -0,0 +1,56 @@ +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "monitoring.fullname" -}} +{{ template "policyreporter.fullname" . }}-monitoring +{{- end }} + +{{- define "monitoring.name" -}} +{{ template "policyreporter.name" . }}-monitoring +{{- end }} + + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "monitoring.chart" -}} +{{ template "policyreporter.chart" . }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "monitoring.labels" -}} +helm.sh/chart: {{ include "monitoring.chart" . }} +{{ include "monitoring.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/component: monitoring +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/part-of: kyverno +{{- with .Values.global.labels }} +{{ toYaml . }} +{{- end -}} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "monitoring.selectorLabels" -}} +app.kubernetes.io/name: {{ include "monitoring.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* Get the namespace name. */}} +{{- define "monitoring.smNamespace" -}} +{{- if .Values.monitoring.serviceMonitor.namespace -}} +{{- .Values.monitoring.serviceMonitor.namespace -}} +{{- else if .Values.namespaceOverride -}} + {{- .Values.namespaceOverride -}} +{{- else -}} +{{- .Release.Namespace -}} +{{- end }} +{{- end }} diff --git a/charts/policy-reporter/templates/monitoring/auth-secret.yaml b/charts/policy-reporter/templates/monitoring/auth-secret.yaml new file mode 100644 index 00000000..c9b72db1 --- /dev/null +++ b/charts/policy-reporter/templates/monitoring/auth-secret.yaml @@ -0,0 +1,19 @@ +{{- if and .Values.monitoring.enabled }} +{{- if and .Values.basicAuth.username .Values.basicAuth.password }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "monitoring.fullname" . }}-auth + namespace: {{ include "monitoring.smNamespace" . }} + {{- if .Values.monitoring.annotations }} + annotations: + {{- toYaml .Values.monitoring.annotations | nindent 4 }} + {{- end }} + labels: + {{- include "monitoring.labels" . | nindent 4 }} +type: Opaque +data: + username: {{ .Values.basicAuth.username | b64enc }} + password: {{ .Values.basicAuth.password | b64enc }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/charts/monitoring/templates/clusterpolicy-details.dashboard.yaml b/charts/policy-reporter/templates/monitoring/clusterpolicy-details.dashboard.yaml similarity index 82% rename from charts/policy-reporter/charts/monitoring/templates/clusterpolicy-details.dashboard.yaml rename to charts/policy-reporter/templates/monitoring/clusterpolicy-details.dashboard.yaml index 8003a1c9..626de0e4 100644 --- a/charts/policy-reporter/charts/monitoring/templates/clusterpolicy-details.dashboard.yaml +++ b/charts/policy-reporter/templates/monitoring/clusterpolicy-details.dashboard.yaml @@ -1,11 +1,13 @@ -{{- if and $.Values.grafana.dashboards.enabled $.Values.grafana.dashboards.enable.clusterPolicyReportDetails }} -{{- $filters := .Values.grafana.dashboards.labelFilter }} -{{- if and .Values.grafana.dashboards.multicluster.enabled .Values.grafana.dashboards.multicluster.label }} -{{- $filters = append $filters .Values.grafana.dashboards.multicluster.label }} +{{ $root := .Values.monitoring }} + +{{- if and $root.grafana.dashboards.enabled $root.grafana.dashboards.enable.clusterPolicyReportDetails }} +{{- $filters := $root.grafana.dashboards.labelFilter }} +{{- if and $root.grafana.dashboards.multicluster.enabled $root.grafana.dashboards.multicluster.label }} +{{- $filters = append $filters $root.grafana.dashboards.multicluster.label }} {{- end }} {{- $nsLabel := "exported_namespace" }} -{{- if .Values.serviceMonitor.honorLabels }} +{{- if $root.serviceMonitor.honorLabels }} {{- $nsLabel = "namespace" }} {{- end }} @@ -13,14 +15,14 @@ apiVersion: v1 kind: ConfigMap metadata: name: {{ include "monitoring.fullname" . }}-clusterpolicy-details-dashboard - namespace: {{ include "monitoring.namespace" . }} + namespace: {{ include "policyreporter.namespace" . }} annotations: - {{ .Values.grafana.folder.annotation }}: {{ .Values.grafana.folder.name }} + {{ $root.grafana.folder.annotation }}: {{ $root.grafana.folder.name }} {{- with .Values.annotations }} {{- toYaml . | nindent 4 }} {{- end }} labels: - {{ .Values.grafana.dashboards.label }}: {{ .Values.grafana.dashboards.value | quote }} + {{ $root.grafana.dashboards.label }}: {{ $root.grafana.dashboards.value | quote }} {{- include "monitoring.labels" . | nindent 4 }} data: cluster-policy-reporter-details-dashboard.json: | @@ -28,11 +30,11 @@ data: "__inputs": [ { "name": "DS_PROMETHEUS", - "label": "{{ .Values.grafana.datasource.label }}", + "label": "{{ $root.grafana.datasource.label }}", "description": "", "type": "datasource", - "pluginId": "{{ .Values.grafana.datasource.pluginId }}", - "pluginName": "{{ .Values.grafana.datasource.pluginName }}" + "pluginId": "{{ $root.grafana.datasource.pluginId }}", + "pluginName": "{{ $root.grafana.datasource.pluginName }}" } ], "__requires": [ @@ -101,7 +103,7 @@ data: "overrides": [] }, "gridPos": { - "h": {{ .Values.clusterPolicyReportDetails.statusRow.height }}, + "h": {{ $root.clusterPolicyReportDetails.statusRow.height }}, "w": 6, "x": 0, "y": 0 @@ -124,7 +126,7 @@ data: "pluginVersion": "7.1.5", "targets": [ { - "expr": "sum(cluster_policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", status=\"pass\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} })", + "expr": "max(sum(cluster_policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", status=\"pass\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} }) by (pod))", "instant": true, "interval": "", "legendFormat": "", @@ -158,7 +160,7 @@ data: "overrides": [] }, "gridPos": { - "h": {{ .Values.clusterPolicyReportDetails.statusRow.height }}, + "h": {{ $root.clusterPolicyReportDetails.statusRow.height }}, "w": 6, "x": 6, "y": 0 @@ -181,7 +183,7 @@ data: "pluginVersion": "7.1.5", "targets": [ { - "expr": "sum(cluster_policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", status=\"warn\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} })", + "expr": "max(sum(cluster_policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", status=\"warn\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} }) by (pod))", "instant": true, "interval": "", "legendFormat": "", @@ -215,7 +217,7 @@ data: "overrides": [] }, "gridPos": { - "h": {{ .Values.clusterPolicyReportDetails.statusRow.height }}, + "h": {{ $root.clusterPolicyReportDetails.statusRow.height }}, "w": 6, "x": 12, "y": 0 @@ -238,7 +240,7 @@ data: "pluginVersion": "7.1.5", "targets": [ { - "expr": "sum(cluster_policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", status=\"fail\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} })", + "expr": "max(sum(cluster_policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", status=\"fail\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} }) by (pod))", "instant": true, "interval": "", "legendFormat": "", @@ -272,7 +274,7 @@ data: "overrides": [] }, "gridPos": { - "h": {{ .Values.clusterPolicyReportDetails.statusRow.height }}, + "h": {{ $root.clusterPolicyReportDetails.statusRow.height }}, "w": 6, "x": 18, "y": 0 @@ -295,7 +297,7 @@ data: "pluginVersion": "7.1.5", "targets": [ { - "expr": "sum(cluster_policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", status=\"error\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} })", + "expr": "max(sum(cluster_policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", status=\"error\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} }) by (pod))", "instant": true, "interval": "", "legendFormat": "", @@ -307,7 +309,7 @@ data: "title": "Policy Error Status", "type": "stat" } -{{- if .Values.clusterPolicyReportDetails.statusTimeline.enabled }} +{{- if $root.clusterPolicyReportDetails.statusTimeline.enabled }} ,{ "datasource": { "uid": "${DS_PROMETHEUS}", @@ -412,7 +414,7 @@ data: ] }, "gridPos": { - "h": {{ .Values.clusterPolicyReportDetails.statusTimeline.height }}, + "h": {{ $root.clusterPolicyReportDetails.statusTimeline.height }}, "w": 24, "x": 0, "y": 6 @@ -421,7 +423,7 @@ data: "pluginVersion": "10.4.1", "targets": [ { - "expr": "sum(cluster_policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} } > 0) by (status)", + "expr": "max(sum(cluster_policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} } > 0) by (status, pod)) by (status)", "interval": "", "legendFormat": "{{`{{ status }}`}}", "refId": "A", @@ -451,7 +453,7 @@ data: "timeShift": null } {{- end }} -{{- if .Values.clusterPolicyReportDetails.passTable.enabled }} +{{- if $root.clusterPolicyReportDetails.passTable.enabled }} ,{ "datasource": "${DS_PROMETHEUS}", "fieldConfig": { @@ -477,7 +479,7 @@ data: "overrides": [] }, "gridPos": { - "h": {{ .Values.clusterPolicyReportDetails.passTable.height }}, + "h": {{ $root.clusterPolicyReportDetails.passTable.height }}, "w": 24, "x": 0, "y": 15 @@ -489,7 +491,7 @@ data: "pluginVersion": "7.1.5", "targets": [ { - "expr": "sum(cluster_policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", kind=~\"$kind\", source=~\"$source\", status=\"pass\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} }) by (policy,rule,kind,name,status,severity,category,source{{ range $filters }},{{.}}{{ end }})", + "expr": "max(sum(cluster_policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", kind=~\"$kind\", source=~\"$source\", status=\"pass\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} }) by (pod,policy,rule,kind,name,status,severity,category,source{{ range $filters }},{{.}}{{ end }})) by (policy,rule,kind,name,status,severity,category,source{{ range $filters }},{{.}}{{ end }})", "format": "table", "instant": true, "interval": "", @@ -506,7 +508,6 @@ data: "options": { "excludeByName": { "Time": true, - "Value": true, "status": false }, "indexByName": { @@ -530,7 +531,7 @@ data: "type": "table" } {{- end }} -{{- if .Values.clusterPolicyReportDetails.failTable.enabled }} +{{- if $root.clusterPolicyReportDetails.failTable.enabled }} ,{ "datasource": "${DS_PROMETHEUS}", "fieldConfig": { @@ -556,7 +557,7 @@ data: "overrides": [] }, "gridPos": { - "h": {{ .Values.clusterPolicyReportDetails.failTable.height }}, + "h": {{ $root.clusterPolicyReportDetails.failTable.height }}, "w": 24, "x": 0, "y": 23 @@ -568,7 +569,7 @@ data: "pluginVersion": "7.1.5", "targets": [ { - "expr": "sum(cluster_policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", status=\"fail\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} }) by (policy,rule,kind,name,status,severity,category,source{{ range $filters }},{{.}}{{ end }})", + "expr": "max(sum(cluster_policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", status=\"fail\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} }) by (pod, policy,rule,kind,name,status,severity,category,source{{ range $filters }},{{.}}{{ end }})) by (policy,rule,kind,name,status,severity,category,source{{ range $filters }},{{.}}{{ end }})", "format": "table", "instant": true, "interval": "", @@ -585,7 +586,6 @@ data: "options": { "excludeByName": { "Time": true, - "Value": true, "status": false }, "indexByName": { @@ -609,7 +609,7 @@ data: "type": "table" } {{- end }} -{{- if .Values.clusterPolicyReportDetails.warningTable.enabled }} +{{- if $root.clusterPolicyReportDetails.warningTable.enabled }} ,{ "datasource": "${DS_PROMETHEUS}", "fieldConfig": { @@ -632,7 +632,7 @@ data: "overrides": [] }, "gridPos": { - "h": {{ .Values.clusterPolicyReportDetails.warningTable.height }}, + "h": {{ $root.clusterPolicyReportDetails.warningTable.height }}, "w": 24, "x": 0, "y": 31 @@ -644,7 +644,7 @@ data: "pluginVersion": "7.1.5", "targets": [ { - "expr": "sum(cluster_policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", status=\"warn\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} }) by (policy,rule,kind,name,status,severity,category,source{{ range $filters }},{{.}}{{ end }} )", + "expr": "max(sum(cluster_policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", status=\"warn\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} }) by (pod,policy,rule,kind,name,status,severity,category,source{{ range $filters }},{{.}}{{ end }})) by (policy,rule,kind,name,status,severity,category,source{{ range $filters }},{{.}}{{ end }})", "format": "table", "instant": true, "interval": "", @@ -661,7 +661,6 @@ data: "options": { "excludeByName": { "Time": true, - "Value": true, "status": false }, "indexByName": { @@ -685,7 +684,7 @@ data: "type": "table" } {{- end }} -{{- if .Values.clusterPolicyReportDetails.errorTable.enabled }} +{{- if $root.clusterPolicyReportDetails.errorTable.enabled }} ,{ "datasource": "${DS_PROMETHEUS}", "fieldConfig": { @@ -708,7 +707,7 @@ data: "overrides": [] }, "gridPos": { - "h": {{ .Values.clusterPolicyReportDetails.errorTable.height }}, + "h": {{ $root.clusterPolicyReportDetails.errorTable.height }}, "w": 24, "x": 0, "y": 36 @@ -720,7 +719,7 @@ data: "pluginVersion": "7.1.5", "targets": [ { - "expr": "sum(cluster_policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", status=\"warn\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} }) by (policy,rule,kind,name,status,severity,category,source{{ range $filters }},{{.}}{{ end }})", + "expr": "max(sum(cluster_policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", status=\"warn\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} }) by (pod, policy,rule,kind,name,status,severity,category,source{{ range $filters }},{{.}}{{ end }})) by (policy,rule,kind,name,status,severity,category,source{{ range $filters }},{{.}}{{ end }})", "format": "table", "instant": true, "interval": "", @@ -737,7 +736,6 @@ data: "options": { "excludeByName": { "Time": true, - "Value": true, "status": false }, "indexByName": { diff --git a/charts/policy-reporter/templates/monitoring/clusterpolicy-details.grafanadashboard.yaml b/charts/policy-reporter/templates/monitoring/clusterpolicy-details.grafanadashboard.yaml new file mode 100644 index 00000000..21f5e22b --- /dev/null +++ b/charts/policy-reporter/templates/monitoring/clusterpolicy-details.grafanadashboard.yaml @@ -0,0 +1,19 @@ +{{- if and .Values.monitoring.grafana.dashboards.enabled .Values.monitoring.grafana.dashboards.enable.clusterPolicyReportDetails .Values.monitoring.grafana.grafanaDashboard.enabled }} +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaDashboard +metadata: + labels: + {{ .Values.monitoring.grafana.dashboards.label }}: {{ .Values.monitoring.grafana.dashboards.value | quote }} + {{- include "monitoring.labels" . | nindent 4 }} + name: {{ include "monitoring.fullname" . }}-clusterpolicy-details-dashboard + namespace: {{ include "policyreporter.namespace" . }} +spec: + allowCrossNamespaceImport: {{ .Values.monitoring.grafana.grafanaDashboard.allowCrossNamespaceImport }} + folder: {{ .Values.monitoring.grafana.grafanaDashboard.folder }} + instanceSelector: + matchLabels: + {{- toYaml .Values.monitoring.grafana.grafanaDashboard.matchLabels | nindent 6 }} + configMapRef: + name: {{ include "monitoring.fullname" . }}-clusterpolicy-details-dashboard + key: cluster-policy-reporter-details-dashboard.json +{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/charts/monitoring/templates/overview.dashboard.yaml b/charts/policy-reporter/templates/monitoring/overview.dashboard.yaml similarity index 84% rename from charts/policy-reporter/charts/monitoring/templates/overview.dashboard.yaml rename to charts/policy-reporter/templates/monitoring/overview.dashboard.yaml index 0a1a37f6..4fa1375b 100644 --- a/charts/policy-reporter/charts/monitoring/templates/overview.dashboard.yaml +++ b/charts/policy-reporter/templates/monitoring/overview.dashboard.yaml @@ -1,11 +1,13 @@ -{{- if and $.Values.grafana.dashboards.enabled $.Values.grafana.dashboards.enable.overview }} -{{- $filters := .Values.grafana.dashboards.labelFilter }} -{{- if and .Values.grafana.dashboards.multicluster.enabled .Values.grafana.dashboards.multicluster.label }} -{{- $filters = append $filters .Values.grafana.dashboards.multicluster.label }} +{{ $root := .Values.monitoring }} + +{{- if and $root.grafana.dashboards.enabled $root.grafana.dashboards.enable.overview }} +{{- $filters := $root.grafana.dashboards.labelFilter }} +{{- if and $root.grafana.dashboards.multicluster.enabled $root.grafana.dashboards.multicluster.label }} +{{- $filters = append $filters $root.grafana.dashboards.multicluster.label }} {{- end }} {{- $nsLabel := "exported_namespace" }} -{{- if .Values.serviceMonitor.honorLabels }} +{{- if $root.serviceMonitor.honorLabels }} {{- $nsLabel = "namespace" }} {{- end }} @@ -13,15 +15,15 @@ apiVersion: v1 kind: ConfigMap metadata: name: {{ include "monitoring.fullname" . }}-overview-dashboard - namespace: {{ include "monitoring.namespace" . }} + namespace: {{ include "policyreporter.namespace" . }} annotations: - {{ .Values.grafana.folder.annotation }}: {{ .Values.grafana.folder.name }} + {{ $root.grafana.folder.annotation }}: {{ $root.grafana.folder.name }} {{- with .Values.annotations }} {{- toYaml . | nindent 4 }} {{- end }} labels: - {{ .Values.grafana.dashboards.label }}: {{ .Values.grafana.dashboards.value | quote }} - {{- with .Values.serviceMonitor.labels }} + {{ $root.grafana.dashboards.label }}: {{ $root.grafana.dashboards.value | quote }} + {{- with $root.serviceMonitor.labels }} {{- toYaml . | nindent 4 }} {{- end }} {{- include "monitoring.labels" . | nindent 4 }} @@ -31,11 +33,11 @@ data: "__inputs": [ { "name": "DS_PROMETHEUS", - "label": "{{ .Values.grafana.datasource.label }}", + "label": "{{ $root.grafana.datasource.label }}", "description": "", "type": "datasource", - "pluginId": "{{ .Values.grafana.datasource.pluginId }}", - "pluginName": "{{ .Values.grafana.datasource.pluginName }}" + "pluginId": "{{ $root.grafana.datasource.pluginId }}", + "pluginName": "{{ $root.grafana.datasource.pluginName }}" } ], "__requires": [ @@ -94,7 +96,7 @@ data: "overrides": [] }, "gridPos": { - "h": {{ .Values.policyReportOverview.failingSummaryRow.height }}, + "h": {{ $root.policyReportOverview.failingSummaryRow.height }}, "w": 15, "x": 0, "y": 0 @@ -115,7 +117,7 @@ data: "pluginVersion": "7.1.5", "targets": [ { - "expr": "sum(policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", {{ $nsLabel }}=~\"$namespace\", status=~\"fail|error\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} } > 0) by ({{ $nsLabel }})", + "expr": "max(sum(policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", {{ $nsLabel }}=~\"$namespace\", status=~\"fail|error\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} } > 0) by ({{ $nsLabel }}, pod)) by ({{ $nsLabel }})", "instant": true, "interval": "", "legendFormat": "{{ printf `{{%s}}` $nsLabel }}", @@ -153,7 +155,7 @@ data: "overrides": [] }, "gridPos": { - "h": {{ .Values.policyReportOverview.failingSummaryRow.height }}, + "h": {{ $root.policyReportOverview.failingSummaryRow.height }}, "w": 9, "x": 15, "y": 0 @@ -176,7 +178,7 @@ data: "pluginVersion": "7.1.5", "targets": [ { - "expr": "sum(cluster_policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", status=~\"fail|error\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} } > 0) by (status)", + "expr": "max(sum(cluster_policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", status=~\"fail|error\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} } > 0) by (status, pod)) by (status)", "format": "time_series", "interval": "", "intervalFactor": 1, @@ -292,7 +294,7 @@ data: ] }, "gridPos": { - "h": {{ .Values.policyReportOverview.failingTimeline.height }}, + "h": {{ $root.policyReportOverview.failingTimeline.height }}, "w": 24, "x": 0, "y": 8 @@ -301,7 +303,7 @@ data: "pluginVersion": "10.4.1", "targets": [ { - "expr": "sum(cluster_policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", status=~\"fail|error\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} } > 0) by (policy)", + "expr": "max(sum(cluster_policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", status=~\"fail|error\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} } > 0) by (policy, pod)) by (policy)", "interval": "", "legendFormat": "{{`{{ policy }}`}}", "refId": "A", @@ -310,7 +312,7 @@ data: } }, { - "expr": "sum(policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", {{ $nsLabel }}=~\"$namespace\", status=~\"fail|error\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} } > 0) by (policy)", + "expr": "max(sum(policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", {{ $nsLabel }}=~\"$namespace\", status=~\"fail|error\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} } > 0) by (policy, pod)) by (policy)", "interval": "", "legendFormat": "{{`{{ policy }}`}}", "refId": "B", @@ -363,7 +365,7 @@ data: "overrides": [] }, "gridPos": { - "h": {{ .Values.policyReportOverview.failingPolicyRuleTable.height }}, + "h": {{ $root.policyReportOverview.failingPolicyRuleTable.height }}, "w": 24, "x": 0, "y": 18 @@ -375,7 +377,7 @@ data: "pluginVersion": "7.1.5", "targets": [ { - "expr": "sum(policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", {{ $nsLabel }}=~\"$namespace\", status=~\"fail|error\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} }) by ({{ $nsLabel }},policy,rule,kind,name,status,category,severity,source{{ range $filters }},{{.}}{{ end }})", + "expr": "max(sum(policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", {{ $nsLabel }}=~\"$namespace\", status=~\"fail|error\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} }) by (pod,{{ $nsLabel }},policy,rule,kind,name,status,category,severity,source{{ range $filters }},{{.}}{{ end }})) by ({{ $nsLabel }},policy,rule,kind,name,status,category,severity,source{{ range $filters }},{{.}}{{ end }})", "format": "table", "instant": true, "interval": "", @@ -391,8 +393,7 @@ data: "id": "organize", "options": { "excludeByName": { - "Time": true, - "Value": true + "Time": true }, "indexByName": { "source": 0, @@ -403,7 +404,8 @@ data: "name": 5, "policy": 6, "rule": 7, - "status": 8 + "status": 8, + "Value": 9 }, "renameByName": { "{{ $nsLabel }}": "namespace" @@ -438,7 +440,7 @@ data: "overrides": [] }, "gridPos": { - "h": {{ .Values.policyReportOverview.failingClusterPolicyRuleTable.height }}, + "h": {{ $root.policyReportOverview.failingClusterPolicyRuleTable.height }}, "w": 24, "x": 0, "y": 28 @@ -450,7 +452,7 @@ data: "pluginVersion": "7.1.5", "targets": [ { - "expr": "sum(cluster_policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", status=~\"fail|error\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} }) by (policy,rule,kind,name,status,category,severity,source{{ range $filters }},{{.}}{{ end }})", + "expr": "max(sum(cluster_policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", status=~\"fail|error\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} })by (pod,policy,rule,kind,name,status,category,severity,source{{ range $filters }},{{.}}{{ end }})) by (policy,rule,kind,name,status,category,severity,source{{ range $filters }},{{.}}{{ end }})", "format": "table", "instant": true, "interval": "", @@ -467,7 +469,6 @@ data: "options": { "excludeByName": { "Time": true, - "Value": true, "__name__": true, "endpoint": true, "instance": true, @@ -486,7 +487,8 @@ data: "name": 4, "policy": 5, "rule": 6, - "status": 7 + "status": 7, + "Value": 8 }, "renameByName": {} } diff --git a/charts/policy-reporter/templates/monitoring/overview.grafanadashboard.yaml b/charts/policy-reporter/templates/monitoring/overview.grafanadashboard.yaml new file mode 100644 index 00000000..462d6a22 --- /dev/null +++ b/charts/policy-reporter/templates/monitoring/overview.grafanadashboard.yaml @@ -0,0 +1,19 @@ +{{- if and .Values.monitoring.grafana.dashboards.enabled .Values.monitoring.grafana.dashboards.enable.overview .Values.monitoring.grafana.grafanaDashboard.enabled }} +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaDashboard +metadata: + labels: + {{ .Values.monitoring.grafana.dashboards.label }}: {{ .Values.monitoring.grafana.dashboards.value | quote }} + {{- include "monitoring.labels" . | nindent 4 }} + name: {{ include "monitoring.fullname" . }}-overview-dashboard + namespace: {{ include "policyreporter.namespace" . }} +spec: + allowCrossNamespaceImport: {{ .Values.monitoring.grafana.grafanaDashboard.allowCrossNamespaceImport }} + folder: {{ .Values.monitoring.grafana.grafanaDashboard.folder }} + instanceSelector: + matchLabels: + {{- toYaml .Values.monitoring.grafana.grafanaDashboard.matchLabels | nindent 6 }} + configMapRef: + name: {{ include "monitoring.fullname" . }}-overview-dashboard + key: policy-reporter-dashboard.json +{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/charts/monitoring/templates/policy-details.dashboard.yaml b/charts/policy-reporter/templates/monitoring/policy-details.dashboard.yaml similarity index 81% rename from charts/policy-reporter/charts/monitoring/templates/policy-details.dashboard.yaml rename to charts/policy-reporter/templates/monitoring/policy-details.dashboard.yaml index 6cac5901..024a2238 100644 --- a/charts/policy-reporter/charts/monitoring/templates/policy-details.dashboard.yaml +++ b/charts/policy-reporter/templates/monitoring/policy-details.dashboard.yaml @@ -1,11 +1,13 @@ -{{- if and $.Values.grafana.dashboards.enabled $.Values.grafana.dashboards.enable.policyReportDetails }} -{{- $filters := .Values.grafana.dashboards.labelFilter }} -{{- if and .Values.grafana.dashboards.multicluster.enabled .Values.grafana.dashboards.multicluster.label }} -{{- $filters = append $filters .Values.grafana.dashboards.multicluster.label }} +{{ $root := .Values.monitoring }} + +{{- if and $root.grafana.dashboards.enabled $root.grafana.dashboards.enable.policyReportDetails }} +{{- $filters := $root.grafana.dashboards.labelFilter }} +{{- if and $root.grafana.dashboards.multicluster.enabled $root.grafana.dashboards.multicluster.label }} +{{- $filters = append $filters $root.grafana.dashboards.multicluster.label }} {{- end }} {{- $nsLabel := "exported_namespace" }} -{{- if .Values.serviceMonitor.honorLabels }} +{{- if $root.serviceMonitor.honorLabels }} {{- $nsLabel = "namespace" }} {{- end }} @@ -13,15 +15,15 @@ apiVersion: v1 kind: ConfigMap metadata: name: {{ include "monitoring.fullname" . }}-policy-details-dashboard - namespace: {{ include "monitoring.namespace" . }} + namespace: {{ include "policyreporter.namespace" . }} annotations: - {{ .Values.grafana.folder.annotation }}: {{ .Values.grafana.folder.name }} + {{ $root.grafana.folder.annotation }}: {{ $root.grafana.folder.name }} {{- with .Values.annotations }} {{- toYaml . | nindent 4 }} {{- end }} labels: - {{ .Values.grafana.dashboards.label }}: {{ .Values.grafana.dashboards.value | quote }} - {{- with .Values.serviceMonitor.labels }} + {{ $root.grafana.dashboards.label }}: {{ $root.grafana.dashboards.value | quote }} + {{- with $root.serviceMonitor.labels }} {{- toYaml . | nindent 4 }} {{- end }} {{- include "monitoring.labels" . | nindent 4 }} @@ -31,11 +33,11 @@ data: "__inputs": [ { "name": "DS_PROMETHEUS", - "label": "{{ .Values.grafana.datasource.label }}", + "label": "{{ $root.grafana.datasource.label }}", "description": "", "type": "datasource", - "pluginId": "{{ .Values.grafana.datasource.pluginId }}", - "pluginName": "{{ .Values.grafana.datasource.pluginName }}" + "pluginId": "{{ $root.grafana.datasource.pluginId }}", + "pluginName": "{{ $root.grafana.datasource.pluginName }}" } ], "__requires": [ @@ -104,7 +106,7 @@ data: "overrides": [] }, "gridPos": { - "h": {{ .Values.policyReportDetails.firstStatusRow.height }}, + "h": {{ $root.policyReportDetails.firstStatusRow.height }}, "w": 12, "x": 0, "y": 0 @@ -125,7 +127,7 @@ data: "pluginVersion": "7.1.5", "targets": [ { - "expr": "sum(policy_report_result{policy=~\"$policy\", rule=~\"$rule\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", {{ $nsLabel }}=~\"$namespace\", status=\"pass\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} } > 0) by ({{ $nsLabel }})", + "expr": "max(sum(policy_report_result{policy=~\"$policy\", rule=~\"$rule\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", {{ $nsLabel }}=~\"$namespace\", status=\"pass\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} } > 0) by ({{ $nsLabel }}, pod)) by ({{ $nsLabel }})", "instant": true, "interval": "", "legendFormat": "{{ printf `{{%s}}` $nsLabel }}", @@ -159,7 +161,7 @@ data: "overrides": [] }, "gridPos": { - "h": {{ .Values.policyReportDetails.firstStatusRow.height }}, + "h": {{ $root.policyReportDetails.firstStatusRow.height }}, "w": 12, "x": 12, "y": 0 @@ -180,7 +182,7 @@ data: "pluginVersion": "7.1.5", "targets": [ { - "expr": "sum(policy_report_result{policy=~\"$policy\", rule=~\"$rule\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", {{ $nsLabel }}=~\"$namespace\", status=\"fail\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} } > 0) by ({{ $nsLabel }})", + "expr": "max(sum(policy_report_result{policy=~\"$policy\", rule=~\"$rule\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", {{ $nsLabel }}=~\"$namespace\", status=\"fail\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} } > 0) by ({{ $nsLabel }}, pod)) by ({{ $nsLabel }})", "instant": true, "interval": "", "legendFormat": "{{ printf `{{%s}}` $nsLabel }}", @@ -192,7 +194,7 @@ data: "title": "Policy Fail Status", "type": "bargauge" } -{{- if .Values.policyReportDetails.secondStatusRow.enabled }} +{{- if $root.policyReportDetails.secondStatusRow.enabled }} ,{ "datasource": "${DS_PROMETHEUS}", "fieldConfig": { @@ -215,7 +217,7 @@ data: "overrides": [] }, "gridPos": { - "h": {{ .Values.policyReportDetails.secondStatusRow.height }}, + "h": {{ $root.policyReportDetails.secondStatusRow.height }}, "w": 12, "x": 0, "y": 7 @@ -236,7 +238,7 @@ data: "pluginVersion": "7.1.5", "targets": [ { - "expr": "sum(policy_report_result{policy=~\"$policy\", rule=~\"$rule\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", {{ $nsLabel }}=~\"$namespace\", status=\"warn\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} } > 0) by ({{ $nsLabel }})", + "expr": "max(sum(policy_report_result{policy=~\"$policy\", rule=~\"$rule\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", {{ $nsLabel }}=~\"$namespace\", status=\"warn\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} } > 0) by ({{ $nsLabel }}, pod)) by ({{ $nsLabel }})", "instant": true, "interval": "", "legendFormat": "{{ printf `{{%s}}` $nsLabel }}", @@ -270,7 +272,7 @@ data: "overrides": [] }, "gridPos": { - "h": {{ .Values.policyReportDetails.secondStatusRow.height }}, + "h": {{ $root.policyReportDetails.secondStatusRow.height }}, "w": 12, "x": 12, "y": 7 @@ -291,7 +293,7 @@ data: "pluginVersion": "7.1.5", "targets": [ { - "expr": "sum(policy_report_result{policy=~\"$policy\", rule=~\"$rule\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", {{ $nsLabel }}=~\"$namespace\", status=\"error\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} } > 0) by ({{ $nsLabel }})", + "expr": "max(sum(policy_report_result{policy=~\"$policy\", rule=~\"$rule\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", {{ $nsLabel }}=~\"$namespace\", status=\"error\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} } > 0) by ({{ $nsLabel }}, pod)) by ({{ $nsLabel }})", "instant": true, "interval": "", "legendFormat": "{{ printf `{{%s}}` $nsLabel }}", @@ -304,7 +306,7 @@ data: "type": "bargauge" } {{- end }} -{{- if .Values.policyReportDetails.statusTimeline.enabled }} +{{- if $root.policyReportDetails.statusTimeline.enabled }} ,{ "datasource": { "uid": "${DS_PROMETHEUS}" @@ -408,7 +410,7 @@ data: ] }, "gridPos": { - "h": {{ .Values.policyReportDetails.statusTimeline.height }}, + "h": {{ $root.policyReportDetails.statusTimeline.height }}, "w": 24, "x": 0, "y": 10 @@ -417,7 +419,7 @@ data: "pluginVersion": "10.4.1", "targets": [ { - "expr": "sum(policy_report_result{policy=~\"$policy\", rule=~\"$rule\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", {{ $nsLabel }}=~\"$namespace\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} } > 0) by (status, {{ $nsLabel }})", + "expr": "max(sum(policy_report_result{policy=~\"$policy\", rule=~\"$rule\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", {{ $nsLabel }}=~\"$namespace\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} } > 0) by (status, pod, {{ $nsLabel }})) by (status, {{ $nsLabel }})", "interval": "", "legendFormat": "{{ printf `{{%s}}` $nsLabel }} {{`{{ status }}`}}", "refId": "A", @@ -446,7 +448,7 @@ data: "timeShift": null } {{- end }} -{{- if .Values.policyReportDetails.passTable.enabled }} +{{- if $root.policyReportDetails.passTable.enabled }} ,{ "datasource": "${DS_PROMETHEUS}", "fieldConfig": { @@ -472,7 +474,7 @@ data: "overrides": [] }, "gridPos": { - "h": {{ .Values.policyReportDetails.passTable.height }}, + "h": {{ $root.policyReportDetails.passTable.height }}, "w": 24, "x": 0, "y": 19 @@ -484,7 +486,7 @@ data: "pluginVersion": "7.1.5", "targets": [ { - "expr": "sum(policy_report_result{policy=~\"$policy\", rule=~\"$rule\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", {{ $nsLabel }}=~\"$namespace\", status=\"pass\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} }) by ({{ $nsLabel }},category,policy,rule,kind,name,severity,status,source{{ range $filters }},{{.}}{{ end }} )", + "expr": "max(sum(policy_report_result{policy=~\"$policy\", rule=~\"$rule\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", {{ $nsLabel }}=~\"$namespace\", status=\"pass\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} }) by ({{ $nsLabel }},category,policy,rule,kind,name,severity,status,source{{ range $filters }},{{.}}{{ end }})) by ({{ $nsLabel }},category,policy,rule,kind,name,severity,status,source{{ range $filters }},{{.}}{{ end }})", "format": "table", "instant": true, "interval": "", @@ -501,7 +503,6 @@ data: "options": { "excludeByName": { "Time": true, - "Value": true, "status": false }, "indexByName": { @@ -526,7 +527,7 @@ data: "type": "table" } {{- end }} -{{- if .Values.policyReportDetails.failTable.enabled }} +{{- if $root.policyReportDetails.failTable.enabled }} ,{ "datasource": "${DS_PROMETHEUS}", "fieldConfig": { @@ -552,7 +553,7 @@ data: "overrides": [] }, "gridPos": { - "h": {{ .Values.policyReportDetails.failTable.height }}, + "h": {{ $root.policyReportDetails.failTable.height }}, "w": 24, "x": 0, "y": 27 @@ -564,7 +565,7 @@ data: "pluginVersion": "7.1.5", "targets": [ { - "expr": "sum(policy_report_result{policy=~\"$policy\", rule=~\"$rule\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", {{ $nsLabel }}=~\"$namespace\", status=\"fail\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} }) by ({{ $nsLabel }},category,policy,rule,kind,name,severity,status,source{{ range $filters }},{{.}}{{ end }})", + "expr": "max(sum(policy_report_result{policy=~\"$policy\", rule=~\"$rule\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", {{ $nsLabel }}=~\"$namespace\", status=\"fail\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} }) by ({{ $nsLabel }},category,policy,rule,kind,name,severity,status,source{{ range $filters }},{{.}}{{ end }})) by ({{ $nsLabel }},category,policy,rule,kind,name,severity,status,source{{ range $filters }},{{.}}{{ end }})", "format": "table", "instant": true, "interval": "", @@ -581,7 +582,6 @@ data: "options": { "excludeByName": { "Time": true, - "Value": true, "status": false }, "indexByName": { @@ -606,7 +606,7 @@ data: "type": "table" } {{- end }} -{{- if .Values.policyReportDetails.warningTable.enabled }} +{{- if $root.policyReportDetails.warningTable.enabled }} ,{ "datasource": "${DS_PROMETHEUS}", "fieldConfig": { @@ -629,7 +629,7 @@ data: "overrides": [] }, "gridPos": { - "h": {{ .Values.policyReportDetails.warningTable.height }}, + "h": {{ $root.policyReportDetails.warningTable.height }}, "w": 24, "x": 0, "y": 35 @@ -641,7 +641,7 @@ data: "pluginVersion": "7.1.5", "targets": [ { - "expr": "sum(policy_report_result{policy=~\"$policy\", rule=~\"$rule\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", {{ $nsLabel }}=~\"$namespace\", status=\"warn\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} }) by ({{ $nsLabel }},category,policy,rule,kind,name,severity,status,source{{ range $filters }},{{.}}{{ end }} )", + "expr": "max(sum(policy_report_result{policy=~\"$policy\", rule=~\"$rule\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", {{ $nsLabel }}=~\"$namespace\", status=\"warn\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} }) by ({{ $nsLabel }},category,policy,rule,kind,name,severity,status,source{{ range $filters }},{{.}}{{ end }})) by ({{ $nsLabel }},category,policy,rule,kind,name,severity,status,source{{ range $filters }},{{.}}{{ end }} )", "format": "table", "instant": true, "interval": "", @@ -658,7 +658,6 @@ data: "options": { "excludeByName": { "Time": true, - "Value": true, "status": false }, "indexByName": { @@ -683,7 +682,7 @@ data: "type": "table" } {{- end }} -{{- if .Values.policyReportDetails.errorTable.enabled }} +{{- if $root.policyReportDetails.errorTable.enabled }} ,{ "datasource": "${DS_PROMETHEUS}", "fieldConfig": { @@ -706,7 +705,7 @@ data: "overrides": [] }, "gridPos": { - "h": {{ .Values.policyReportDetails.errorTable.height }}, + "h": {{ $root.policyReportDetails.errorTable.height }}, "w": 24, "x": 0, "y": 40 @@ -718,7 +717,7 @@ data: "pluginVersion": "7.1.5", "targets": [ { - "expr": "sum(policy_report_result{policy=~\"$policy\", rule=~\"$rule\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", {{ $nsLabel }}=~\"$namespace\", status=\"error\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} }) by ({{ $nsLabel }},category,policy,rule,kind,name,severity,status,source{{ range $filters }},{{.}}{{ end }} )", + "expr": "max(sum(policy_report_result{policy=~\"$policy\", rule=~\"$rule\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", {{ $nsLabel }}=~\"$namespace\", status=\"error\"{{ range $filters }}, {{.}}=~\"${{.}}\"{{ end }} }) by ({{ $nsLabel }},category,policy,rule,kind,name,severity,status,source{{ range $filters }},{{.}}{{ end }})) by ({{ $nsLabel }},category,policy,rule,kind,name,severity,status,source{{ range $filters }},{{.}}{{ end }})", "format": "table", "instant": true, "interval": "", @@ -735,12 +734,10 @@ data: "options": { "excludeByName": { "Time": true, - "Value": true, "status": false }, "indexByName": { "Time": 0, - "Value": 9, "category": 1, "{{ $nsLabel }}": 3, "kind": 4, @@ -748,7 +745,8 @@ data: "policy": 6, "rule": 7, "severity": 2, - "status": 8 + "status": 8, + "Value": 9 }, "renameByName": { "{{ $nsLabel }}": "namespace" diff --git a/charts/policy-reporter/templates/monitoring/policy-details.grafanadashboard.yaml b/charts/policy-reporter/templates/monitoring/policy-details.grafanadashboard.yaml new file mode 100644 index 00000000..ce8b66fe --- /dev/null +++ b/charts/policy-reporter/templates/monitoring/policy-details.grafanadashboard.yaml @@ -0,0 +1,19 @@ +{{- if and .Values.monitoring.grafana.dashboards.enabled .Values.monitoring.grafana.dashboards.enable.policyReportDetails .Values.monitoring.grafana.grafanaDashboard.enabled }} +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaDashboard +metadata: + labels: + {{ .Values.monitoring.grafana.dashboards.label }}: {{ .Values.monitoring.grafana.dashboards.value | quote }} + {{- include "monitoring.labels" . | nindent 4 }} + name: {{ include "monitoring.fullname" . }}-policy-details-dashboard + namespace: {{ include "policyreporter.namespace" . }} +spec: + allowCrossNamespaceImport: {{ .Values.monitoring.grafana.grafanaDashboard.allowCrossNamespaceImport }} + folder: {{ .Values.monitoring.grafana.grafanaDashboard.folder }} + instanceSelector: + matchLabels: + {{- toYaml .Values.monitoring.grafana.grafanaDashboard.matchLabels | nindent 6 }} + configMapRef: + name: {{ include "monitoring.fullname" . }}-policy-details-dashboard + key: policy-reporter-details-dashboard.json +{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/charts/monitoring/templates/servicemonitor.yaml b/charts/policy-reporter/templates/monitoring/servicemonitor.yaml similarity index 50% rename from charts/policy-reporter/charts/monitoring/templates/servicemonitor.yaml rename to charts/policy-reporter/templates/monitoring/servicemonitor.yaml index 16a0f489..8f5e3fa8 100644 --- a/charts/policy-reporter/charts/monitoring/templates/servicemonitor.yaml +++ b/charts/policy-reporter/templates/monitoring/servicemonitor.yaml @@ -1,28 +1,29 @@ +{{- if and .Values.monitoring.enabled }} apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: name: {{ include "monitoring.fullname" . }} namespace: {{ include "monitoring.smNamespace" . }} - {{- if .Values.annotations }} + {{- if .Values.monitoring.annotations }} annotations: - {{- toYaml .Values.annotations | nindent 4 }} + {{- toYaml .Values.monitoring.annotations | nindent 4 }} {{- end }} labels: {{- include "monitoring.labels" . | nindent 4 }} - {{- with .Values.serviceMonitor.labels }} + {{- with .Values.monitoring.serviceMonitor.labels }} {{- toYaml . | nindent 4 }} {{- end }} spec: selector: matchLabels: {{- include "policyreporter.selectorLabels" . | nindent 8 }} - {{- with .Values.serviceMonitor.namespaceSelector }} + {{- with .Values.monitoring.serviceMonitor.namespaceSelector }} namespaceSelector: {{- toYaml . | nindent 4 }} {{- end }} endpoints: - port: http - {{- if and .Values.global.basicAuth.username .Values.global.basicAuth.password }} + {{- if and .Values.basicAuth.username .Values.basicAuth.password }} basicAuth: password: name: {{ include "monitoring.fullname" . }}-auth @@ -30,32 +31,28 @@ spec: username: name: {{ include "monitoring.fullname" . }}-auth key: username - {{- else if .Values.global.basicAuth.secretRef }} + {{- else if .Values.basicAuth.secretRef }} basicAuth: password: - name: {{ .Values.global.basicAuth.secretRef }} + name: {{ .Values.basicAuth.secretRef }} key: password username: - name: {{ .Values.global.basicAuth.secretRef }} + name: {{ .Values.basicAuth.secretRef }} key: username {{- end }} - honorLabels: {{ .Values.serviceMonitor.honorLabels }} - {{- if .Values.serviceMonitor.scrapeTimeout }} - scrapeTimeout: {{ .Values.serviceMonitor.scrapeTimeout }} + honorLabels: {{ .Values.monitoring.serviceMonitor.honorLabels }} + {{- if .Values.monitoring.serviceMonitor.scrapeTimeout }} + scrapeTimeout: {{ .Values.monitoring.serviceMonitor.scrapeTimeout }} {{- end }} - {{- if .Values.serviceMonitor.interval }} - interval: {{ .Values.serviceMonitor.interval }} + {{- if .Values.monitoring.serviceMonitor.interval }} + interval: {{ .Values.monitoring.serviceMonitor.interval }} {{- end }} + {{- with .Values.monitoring.serviceMonitor.relabelings }} relabelings: - - action: labeldrop - regex: pod|service|container - - targetLabel: instance - replacement: policy-reporter - action: replace - {{- with .Values.serviceMonitor.relabelings }} {{- toYaml . | nindent 4 }} {{- end }} - {{- with .Values.serviceMonitor.metricRelabelings }} + {{- with .Values.monitoring.serviceMonitor.metricRelabelings }} metricRelabelings: {{- toYaml . | nindent 4 }} {{- end }} +{{- end }} diff --git a/charts/policy-reporter/templates/networkpolicy.yaml b/charts/policy-reporter/templates/networkpolicy.yaml index ac09030a..b1ffdad6 100644 --- a/charts/policy-reporter/templates/networkpolicy.yaml +++ b/charts/policy-reporter/templates/networkpolicy.yaml @@ -22,21 +22,21 @@ spec: matchLabels: {{- include "ui.selectorLabels" . | nindent 10 }} ports: - protocol: TCP - port: 8080 + port: {{ .Values.ui.service.port }} + {{- end }} + {{- if .Values.plugin.trivy.enabled }} + - from: + - podSelector: + matchLabels: {{- include "trivy-plugin.selectorLabels" . | nindent 10 }} + ports: + - protocol: TCP + port: {{ .Values.plugin.trivy.service.port }} {{- end }} {{- with .Values.networkPolicy.ingress }} {{- toYaml . | nindent 2 }} {{- end }} - egress: - {{- if .Values.ui.enabled }} - - to: - - podSelector: - matchLabels: {{- include "ui.selectorLabels" . | nindent 10 }} - ports: - - protocol: TCP - port: {{ .Values.ui.service.port }} - {{- end }} {{- with .Values.networkPolicy.egress }} + egress: {{- toYaml . | nindent 2 }} {{- end }} {{- end }} diff --git a/charts/policy-reporter/templates/plugins/kyverno/_helpers.tpl b/charts/policy-reporter/templates/plugins/kyverno/_helpers.tpl new file mode 100644 index 00000000..d462c0fd --- /dev/null +++ b/charts/policy-reporter/templates/plugins/kyverno/_helpers.tpl @@ -0,0 +1,68 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "kyverno-plugin.name" -}} +{{ template "policyreporter.name" . }}-kyverno-plugin +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "kyverno-plugin.fullname" -}} +{{ template "policyreporter.fullname" . }}-kyverno-plugin +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "kyverno-plugin.chart" -}} +{{ template "policyreporter.chart" . }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "kyverno-plugin.labels" -}} +helm.sh/chart: {{ include "kyverno-plugin.chart" . }} +{{ include "kyverno-plugin.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- with .Values.global.labels }} +{{ toYaml . }} +{{- end -}} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "kyverno-plugin.selectorLabels" -}} +app.kubernetes.io/name: {{ include "kyverno-plugin.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "kyverno-plugin.serviceAccountName" -}} +{{- if .Values.plugin.kyverno.serviceAccount.create }} +{{- default (include "kyverno-plugin.fullname" .) .Values.plugin.kyverno.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.plugin.kyverno.serviceAccount.name }} +{{- end }} +{{- end }} + +{{- define "kyverno-plugin.podDisruptionBudget" -}} +{{- if and .Values.plugin.kyverno.podDisruptionBudget.minAvailable .Values.plugin.kyverno.podDisruptionBudget.maxUnavailable }} +{{- fail "Cannot set both" -}} +{{- end }} +{{- if not .Values.plugin.kyverno.podDisruptionBudget.maxUnavailable }} +minAvailable: {{ default 1 .Values.plugin.kyverno.podDisruptionBudget.minAvailable }} +{{- end }} +{{- if .Values.plugin.kyverno.podDisruptionBudget.maxUnavailable }} +maxUnavailable: {{ .Values.plugin.kyverno.podDisruptionBudget.maxUnavailable }} +{{- end }} +{{- end }} diff --git a/charts/policy-reporter/charts/kyvernoPlugin/templates/clusterrole.yaml b/charts/policy-reporter/templates/plugins/kyverno/clusterrole.yaml similarity index 57% rename from charts/policy-reporter/charts/kyvernoPlugin/templates/clusterrole.yaml rename to charts/policy-reporter/templates/plugins/kyverno/clusterrole.yaml index 94002565..128b33e0 100644 --- a/charts/policy-reporter/charts/kyvernoPlugin/templates/clusterrole.yaml +++ b/charts/policy-reporter/templates/plugins/kyverno/clusterrole.yaml @@ -1,15 +1,12 @@ -{{- if .Values.rbac.enabled -}} +{{- if .Values.plugin.kyverno.enabled -}} +{{- if .Values.plugin.kyverno.rbac.enabled -}} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - {{- if .Values.annotations }} - annotations: - {{- toYaml .Values.annotations | nindent 4 }} - {{- end }} labels: rbac.authorization.k8s.io/aggregate-to-admin: "true" - {{- include "kyvernoplugin.labels" . | nindent 4 }} - name: {{ include "kyvernoplugin.fullname" . }} + {{- include "kyverno-plugin.labels" . | nindent 4 }} + name: {{ include "kyverno-plugin.fullname" . }} rules: - apiGroups: - '*' @@ -21,8 +18,7 @@ rules: verbs: - get - list - - watch -{{- if .Values.blockReports.enabled }} +{{- if .Values.plugin.kyverno.blockReports.enabled }} - apiGroups: - "" resources: @@ -44,14 +40,6 @@ rules: - create - update - delete -{{- else }} -- apiGroups: - - '*' - resources: - - policyreports - - clusterpolicyreports - verbs: - - get - - list {{- end }} -{{- end -}} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/templates/plugins/kyverno/clusterrolebinding.yaml b/charts/policy-reporter/templates/plugins/kyverno/clusterrolebinding.yaml new file mode 100644 index 00000000..fb7ed840 --- /dev/null +++ b/charts/policy-reporter/templates/plugins/kyverno/clusterrolebinding.yaml @@ -0,0 +1,18 @@ +{{- if .Values.plugin.kyverno.enabled -}} +{{- if and .Values.plugin.kyverno.serviceAccount.create .Values.plugin.kyverno.rbac.enabled -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "kyverno-plugin.fullname" . }} + labels: + {{- include "kyverno-plugin.labels" . | nindent 4 }} +roleRef: + kind: ClusterRole + name: {{ include "kyverno-plugin.fullname" . }} + apiGroup: rbac.authorization.k8s.io +subjects: +- kind: "ServiceAccount" + name: {{ include "kyverno-plugin.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/templates/plugins/kyverno/config-secret.yaml b/charts/policy-reporter/templates/plugins/kyverno/config-secret.yaml new file mode 100644 index 00000000..6fcc3698 --- /dev/null +++ b/charts/policy-reporter/templates/plugins/kyverno/config-secret.yaml @@ -0,0 +1,12 @@ +{{- if .Values.plugin.kyverno.enabled -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "kyverno-plugin.fullname" . }}-config + namespace: {{ include "policyreporter.namespace" . }} + labels: + {{- include "kyverno-plugin.labels" . | nindent 4 }} +type: Opaque +data: + config.yaml: {{ tpl (.Files.Get "configs/kyverno-plugin.tmpl") . | b64enc }} +{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/templates/plugins/kyverno/deployment.yaml b/charts/policy-reporter/templates/plugins/kyverno/deployment.yaml new file mode 100644 index 00000000..64e10133 --- /dev/null +++ b/charts/policy-reporter/templates/plugins/kyverno/deployment.yaml @@ -0,0 +1,103 @@ +{{- if .Values.plugin.kyverno.enabled -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "kyverno-plugin.fullname" . }} + namespace: {{ include "policyreporter.namespace" . }} + labels: + {{- include "kyverno-plugin.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.plugin.kyverno.replicaCount }} + revisionHistoryLimit: {{ .Values.plugin.kyverno.revisionHistoryLimit }} + {{- with .Values.plugin.kyverno.updateStrategy }} + strategy: + {{- toYaml . | nindent 4 }} + {{- end }} + selector: + matchLabels: + {{- include "kyverno-plugin.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + checksum/secret: {{ include (print .Template.BasePath "/plugins/kyverno/config-secret.yaml") . | sha256sum | quote }} + {{- with .Values.plugin.kyverno.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "kyverno-plugin.labels" . | nindent 8 }} + {{- with .Values.plugin.kyverno.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.plugin.kyverno.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "kyverno-plugin.serviceAccountName" . }} + {{- if .Values.plugin.kyverno.podSecurityContext }} + securityContext: + {{- toYaml .Values.plugin.kyverno.podSecurityContext | nindent 8 }} + {{- end }} + containers: + - name: policy-reporter-kyverno-plugin + {{- if .Values.plugin.kyverno.securityContext }} + securityContext: + {{- toYaml .Values.plugin.kyverno.securityContext | nindent 12 }} + {{- end }} + image: "{{ .Values.plugin.kyverno.image.registry }}/{{ .Values.plugin.kyverno.image.repository }}:{{ .Values.plugin.kyverno.image.tag }}" + imagePullPolicy: {{ .Values.plugin.kyverno.image.pullPolicy }} + args: + - run + - --config=/app/config.yaml + - --port={{ .Values.plugin.kyverno.server.port }} + ports: + - name: http + containerPort: {{ .Values.plugin.kyverno.server.port }} + protocol: TCP + livenessProbe: + httpGet: + path: /api/v1/policies + port: http + readinessProbe: + httpGet: + path: /api/v1/policies + port: http + resources: + {{- toYaml .Values.plugin.kyverno.resources | nindent 12 }} + volumeMounts: + - name: config-file + mountPath: /app/config.yaml + subPath: config.yaml + readOnly: true + env: + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + {{- if gt (int .Values.plugin.kyverno.replicaCount) 1 }} + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + {{- end }} + {{- with .Values.plugin.kyverno.envVars }} + {{- . | toYaml | trim | nindent 10 }} + {{- end }} + volumes: + - name: config-file + secret: + secretName: {{ include "kyverno-plugin.fullname" . }}-config + optional: true + {{- with .Values.plugin.kyverno.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.plugin.kyverno.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.plugin.kyverno.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/charts/policy-reporter/templates/plugins/kyverno/ingress.yaml b/charts/policy-reporter/templates/plugins/kyverno/ingress.yaml new file mode 100644 index 00000000..36f7ce05 --- /dev/null +++ b/charts/policy-reporter/templates/plugins/kyverno/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.plugin.kyverno.enabled -}} +{{- if .Values.plugin.kyverno.ingress.enabled -}} +{{- $fullName := include "kyverno-plugin.fullname" . -}} +{{- $svcPort := .Values.plugin.kyverno.service.port -}} +{{- if and .Values.plugin.kyverno.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.plugin.kyverno.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.plugin.kyverno.ingress.annotations "kubernetes.io/ingress.class" .Values.plugin.kyverno.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + namespace: {{ include "policyreporter.namespace" . }} + labels: + {{- include "kyverno-plugin.labels" . | nindent 4 }} + {{- with .Values.plugin.kyverno.ingress.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.plugin.kyverno.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.plugin.kyverno.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.plugin.kyverno.ingress.className }} + {{- end }} + {{- if .Values.plugin.kyverno.ingress.tls }} + tls: + {{- toYaml .Values.plugin.kyverno.ingress.tls | nindent 4 }} + {{- end }} + rules: + {{- range .Values.plugin.kyverno.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} +{{- end }} diff --git a/charts/policy-reporter/templates/plugins/kyverno/networkpolicy.yaml b/charts/policy-reporter/templates/plugins/kyverno/networkpolicy.yaml new file mode 100644 index 00000000..f08234fa --- /dev/null +++ b/charts/policy-reporter/templates/plugins/kyverno/networkpolicy.yaml @@ -0,0 +1,24 @@ +{{- if .Values.plugin.kyverno.enabled -}} +{{- if .Values.plugin.kyverno.networkPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + labels: {{- include "kyverno-plugin.labels" . | nindent 4 }} + name: {{ include "kyverno-plugin.fullname" . }} + namespace: {{ include "policyreporter.namespace" . }} +spec: + podSelector: + matchLabels: {{- include "kyverno-plugin.selectorLabels" . | nindent 6 }} + policyTypes: + - Ingress + - Egress + {{- with .Values.plugin.kyverno.networkPolicy.ingress }} + ingress: + {{- toYaml . | nindent 2 }} + {{- end }} + {{- with .Values.plugin.kyverno.networkPolicy.egress }} + egress: + {{- toYaml . | nindent 2 }} + {{- end }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/templates/plugins/kyverno/poddisruptionbudget.yaml b/charts/policy-reporter/templates/plugins/kyverno/poddisruptionbudget.yaml new file mode 100644 index 00000000..ce439f73 --- /dev/null +++ b/charts/policy-reporter/templates/plugins/kyverno/poddisruptionbudget.yaml @@ -0,0 +1,19 @@ +{{- if .Values.plugin.kyverno.enabled -}} +{{- if (gt (int .Values.plugin.kyverno.replicaCount) 1) }} +{{- if .Capabilities.APIVersions.Has "policy/v1/PodDisruptionBudget" }} +apiVersion: policy/v1 +{{- else }} +apiVersion: policy/v1beta1 +{{- end }} +kind: PodDisruptionBudget +metadata: + name: {{ include "kyverno-plugin.fullname" . }} + labels: + {{- include "kyverno-plugin.labels" . | nindent 4 }} +spec: +{{- include "kyverno-plugin.podDisruptionBudget" . | indent 2 }} + selector: + matchLabels: + {{- include "kyverno-plugin.selectorLabels" . | nindent 6 }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/templates/plugins/kyverno/role.yaml b/charts/policy-reporter/templates/plugins/kyverno/role.yaml new file mode 100644 index 00000000..be0b8585 --- /dev/null +++ b/charts/policy-reporter/templates/plugins/kyverno/role.yaml @@ -0,0 +1,22 @@ +{{- if .Values.plugin.kyverno.enabled -}} +{{- if and (and .Values.plugin.kyverno.serviceAccount.create .Values.plugin.kyverno.rbac.enabled) (and .Values.plugin.kyverno.blockReports.enabled (gt (int .Values.plugin.kyverno.replicaCount) 1)) -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + {{- include "kyverno-plugin.labels" . | nindent 4 }} + name: {{ include "kyverno-plugin.fullname" . }}-leaderelection + namespace: {{ include "policyreporter.namespace" . }} +rules: +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - delete + - get + - patch + - update +{{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/templates/plugins/kyverno/rolebinding.yaml b/charts/policy-reporter/templates/plugins/kyverno/rolebinding.yaml new file mode 100644 index 00000000..0af9ec97 --- /dev/null +++ b/charts/policy-reporter/templates/plugins/kyverno/rolebinding.yaml @@ -0,0 +1,19 @@ +{{- if .Values.plugin.kyverno.enabled -}} +{{- if and (and .Values.plugin.kyverno.serviceAccount.create .Values.plugin.kyverno.rbac.enabled) (and .Values.plugin.kyverno.blockReports.enabled (gt (int .Values.plugin.kyverno.replicaCount) 1)) -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "kyverno-plugin.fullname" . }}-leaderelection + namespace: {{ include "policyreporter.namespace" . }} + labels: + {{- include "kyverno-plugin.labels" . | nindent 4 }} +roleRef: + kind: Role + name: {{ include "kyverno-plugin.fullname" . }}-leaderelection + apiGroup: rbac.authorization.k8s.io +subjects: +- kind: "ServiceAccount" + name: {{ include "kyverno-plugin.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/templates/plugins/kyverno/secret-role.yaml b/charts/policy-reporter/templates/plugins/kyverno/secret-role.yaml new file mode 100644 index 00000000..41e20ed2 --- /dev/null +++ b/charts/policy-reporter/templates/plugins/kyverno/secret-role.yaml @@ -0,0 +1,17 @@ +{{- if .Values.plugin.kyverno.enabled -}} +{{- if and .Values.plugin.kyverno.serviceAccount.create .Values.plugin.kyverno.rbac.enabled -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + {{- include "kyverno-plugin.labels" . | nindent 4 }} + name: {{ include "kyverno-plugin.fullname" . }}-secret-reader + namespace: {{ include "policyreporter.namespace" . }} +rules: +- apiGroups: [''] + resources: + - secrets + verbs: + - get +{{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/templates/plugins/kyverno/secret-rolebinding.yaml b/charts/policy-reporter/templates/plugins/kyverno/secret-rolebinding.yaml new file mode 100644 index 00000000..47a096f9 --- /dev/null +++ b/charts/policy-reporter/templates/plugins/kyverno/secret-rolebinding.yaml @@ -0,0 +1,19 @@ +{{- if .Values.plugin.kyverno.enabled -}} +{{- if and .Values.plugin.kyverno.serviceAccount.create .Values.plugin.kyverno.rbac.enabled -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "kyverno-plugin.fullname" . }}-secret-reader + namespace: {{ include "policyreporter.namespace" . }} + labels: + {{- include "kyverno-plugin.labels" . | nindent 4 }} +roleRef: + kind: Role + name: {{ include "kyverno-plugin.fullname" . }}-secret-reader + apiGroup: rbac.authorization.k8s.io +subjects: +- kind: "ServiceAccount" + name: {{ include "kyverno-plugin.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/templates/plugins/kyverno/service.yaml b/charts/policy-reporter/templates/plugins/kyverno/service.yaml new file mode 100644 index 00000000..8f9229e9 --- /dev/null +++ b/charts/policy-reporter/templates/plugins/kyverno/service.yaml @@ -0,0 +1,25 @@ +{{- if .Values.plugin.kyverno.enabled -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "kyverno-plugin.fullname" . }} + namespace: {{ include "policyreporter.namespace" . }} + labels: + {{- include "kyverno-plugin.labels" . | nindent 4 }} + {{- with .Values.plugin.kyverno.service.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.plugin.kyverno.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.plugin.kyverno.service.type }} + ports: + - port: {{ .Values.plugin.kyverno.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "kyverno-plugin.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/charts/policy-reporter/templates/plugins/kyverno/serviceaccount.yaml b/charts/policy-reporter/templates/plugins/kyverno/serviceaccount.yaml new file mode 100644 index 00000000..f790bc81 --- /dev/null +++ b/charts/policy-reporter/templates/plugins/kyverno/serviceaccount.yaml @@ -0,0 +1,16 @@ +{{- if .Values.plugin.kyverno.enabled -}} +{{- if .Values.plugin.kyverno.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "kyverno-plugin.serviceAccountName" . }} + namespace: {{ include "policyreporter.namespace" . }} + labels: + {{- include "kyverno-plugin.labels" . | nindent 4 }} + {{- with .Values.plugin.kyverno.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.plugin.kyverno.serviceAccount.automount }} +{{- end }} +{{- end }} diff --git a/charts/policy-reporter/templates/plugins/trivy/_helpers.tpl b/charts/policy-reporter/templates/plugins/trivy/_helpers.tpl new file mode 100644 index 00000000..2beb8335 --- /dev/null +++ b/charts/policy-reporter/templates/plugins/trivy/_helpers.tpl @@ -0,0 +1,68 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "trivy-plugin.name" -}} +{{ template "policyreporter.name" . }}-trivy-plugin +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "trivy-plugin.fullname" -}} +{{ template "policyreporter.fullname" . }}-trivy-plugin +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "trivy-plugin.chart" -}} +{{ template "policyreporter.chart" . }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "trivy-plugin.labels" -}} +helm.sh/chart: {{ include "trivy-plugin.chart" . }} +{{ include "trivy-plugin.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- with .Values.global.labels }} +{{ toYaml . }} +{{- end -}} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "trivy-plugin.selectorLabels" -}} +app.kubernetes.io/name: {{ include "trivy-plugin.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "trivy-plugin.serviceAccountName" -}} +{{- if .Values.plugin.trivy.serviceAccount.create }} +{{- default (include "trivy-plugin.fullname" .) .Values.plugin.trivy.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.plugin.trivy.serviceAccount.name }} +{{- end }} +{{- end }} + +{{- define "trivy-plugin.podDisruptionBudget" -}} +{{- if and .Values.plugin.trivy.podDisruptionBudget.minAvailable .Values.plugin.trivy.podDisruptionBudget.maxUnavailable }} +{{- fail "Cannot set both" -}} +{{- end }} +{{- if not .Values.plugin.trivy.podDisruptionBudget.maxUnavailable }} +minAvailable: {{ default 1 .Values.plugin.trivy.podDisruptionBudget.minAvailable }} +{{- end }} +{{- if .Values.plugin.trivy.podDisruptionBudget.maxUnavailable }} +maxUnavailable: {{ .Values.plugin.trivy.podDisruptionBudget.maxUnavailable }} +{{- end }} +{{- end }} diff --git a/charts/policy-reporter/templates/plugins/trivy/config-secret.yaml b/charts/policy-reporter/templates/plugins/trivy/config-secret.yaml new file mode 100644 index 00000000..15b86c4a --- /dev/null +++ b/charts/policy-reporter/templates/plugins/trivy/config-secret.yaml @@ -0,0 +1,12 @@ +{{- if .Values.plugin.trivy.enabled -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "trivy-plugin.fullname" . }}-config + namespace: {{ include "policyreporter.namespace" . }} + labels: + {{- include "trivy-plugin.labels" . | nindent 4 }} +type: Opaque +data: + config.yaml: {{ tpl (.Files.Get "configs/trivy-plugin.tmpl") . | b64enc }} +{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/templates/plugins/trivy/deployment.yaml b/charts/policy-reporter/templates/plugins/trivy/deployment.yaml new file mode 100644 index 00000000..b19184d8 --- /dev/null +++ b/charts/policy-reporter/templates/plugins/trivy/deployment.yaml @@ -0,0 +1,97 @@ +{{- if .Values.plugin.trivy.enabled -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "trivy-plugin.fullname" . }} + namespace: {{ include "policyreporter.namespace" . }} + labels: + {{- include "trivy-plugin.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.plugin.trivy.replicaCount }} + revisionHistoryLimit: {{ .Values.plugin.trivy.revisionHistoryLimit }} + {{- with .Values.plugin.trivy.updateStrategy }} + strategy: + {{- toYaml . | nindent 4 }} + {{- end }} + selector: + matchLabels: + {{- include "trivy-plugin.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + checksum/secret: {{ include (print .Template.BasePath "/plugins/trivy/config-secret.yaml") . | sha256sum | quote }} + {{- with .Values.plugin.trivy.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "trivy-plugin.labels" . | nindent 8 }} + {{- with .Values.plugin.trivy.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.plugin.trivy.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "trivy-plugin.serviceAccountName" . }} + {{- if .Values.plugin.trivy.podSecurityContext }} + securityContext: + {{- toYaml .Values.plugin.trivy.podSecurityContext | nindent 8 }} + {{- end }} + containers: + - name: policy-reporter-trivy-plugin + {{- if .Values.plugin.trivy.securityContext }} + securityContext: + {{- toYaml .Values.plugin.trivy.securityContext | nindent 12 }} + {{- end }} + image: "{{ .Values.plugin.trivy.image.registry }}/{{ .Values.plugin.trivy.image.repository }}:{{ .Values.plugin.trivy.image.tag }}" + imagePullPolicy: {{ .Values.plugin.trivy.image.pullPolicy }} + args: + - run + - --config=/app/config.yaml + - --port={{ .Values.plugin.trivy.server.port }} + ports: + - name: http + containerPort: {{ .Values.plugin.trivy.server.port }} + protocol: TCP + livenessProbe: + httpGet: + path: /api/vulnr/v1/policies + port: http + readinessProbe: + httpGet: + path: /api/vulnr/v1/policies + port: http + resources: + {{- toYaml .Values.plugin.trivy.resources | nindent 12 }} + volumeMounts: + - name: config-file + mountPath: /app/config.yaml + subPath: config.yaml + readOnly: true + env: + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + {{- with .Values.plugin.trivy.envVars }} + {{- . | toYaml | trim | nindent 10 }} + {{- end }} + volumes: + - name: config-file + secret: + secretName: {{ include "trivy-plugin.fullname" . }}-config + optional: true + {{- with .Values.plugin.trivy.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.plugin.trivy.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.plugin.trivy.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/templates/plugins/trivy/ingress.yaml b/charts/policy-reporter/templates/plugins/trivy/ingress.yaml new file mode 100644 index 00000000..d7a61acc --- /dev/null +++ b/charts/policy-reporter/templates/plugins/trivy/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.plugin.trivy.enabled -}} +{{- if .Values.plugin.trivy.ingress.enabled -}} +{{- $fullName := include "trivy-plugin.fullname" . -}} +{{- $svcPort := .Values.plugin.trivy.service.port -}} +{{- if and .Values.plugin.trivy.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.plugin.trivy.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.plugin.trivy.ingress.annotations "kubernetes.io/ingress.class" .Values.plugin.trivy.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + namespace: {{ include "policyreporter.namespace" . }} + labels: + {{- include "trivy-plugin.labels" . | nindent 4 }} + {{- with .Values.plugin.trivy.ingress.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.plugin.trivy.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.plugin.trivy.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.plugin.trivy.ingress.className }} + {{- end }} + {{- if .Values.plugin.trivy.ingress.tls }} + tls: + {{- toYaml .Values.plugin.trivy.ingress.tls | nindent 4 }} + {{- end }} + rules: + {{- range .Values.plugin.trivy.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/templates/plugins/trivy/networkpolicy.yaml b/charts/policy-reporter/templates/plugins/trivy/networkpolicy.yaml new file mode 100644 index 00000000..011753a9 --- /dev/null +++ b/charts/policy-reporter/templates/plugins/trivy/networkpolicy.yaml @@ -0,0 +1,31 @@ +{{- if .Values.plugin.trivy.enabled -}} +{{- if .Values.plugin.trivy.networkPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + labels: {{- include "trivy-plugin.labels" . | nindent 4 }} + name: {{ include "trivy-plugin.fullname" . }} + namespace: {{ include "policyreporter.namespace" . }} +spec: + podSelector: + matchLabels: {{- include "trivy-plugin.selectorLabels" . | nindent 6 }} + policyTypes: + - Ingress + - Egress + {{- with .Values.plugin.trivy.networkPolicy.ingress }} + ingress: + {{- toYaml . | nindent 2 }} + {{- end }} + egress: + - to: + - podSelector: + matchLabels: + {{- include "policyreporter.selectorLabels" . | nindent 10 }} + ports: + - protocol: TCP + port: {{ .Values.service.port }} + {{- with .Values.plugin.trivy.networkPolicy.egress }} + {{- toYaml . | nindent 2 }} + {{- end }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/templates/plugins/trivy/poddisruptionbudget.yaml b/charts/policy-reporter/templates/plugins/trivy/poddisruptionbudget.yaml new file mode 100644 index 00000000..52853660 --- /dev/null +++ b/charts/policy-reporter/templates/plugins/trivy/poddisruptionbudget.yaml @@ -0,0 +1,20 @@ +{{- if .Values.plugin.trivy.enabled -}} +{{- if (gt (int .Values.plugin.trivy.replicaCount) 1) }} +{{- if .Capabilities.APIVersions.Has "policy/v1/PodDisruptionBudget" }} +apiVersion: policy/v1 +{{- else }} +apiVersion: policy/v1beta1 +{{- end }} +kind: PodDisruptionBudget +metadata: + name: {{ include "trivy-plugin.fullname" . }} + namespace: {{ include "policyreporter.namespace" . }} + labels: + {{- include "trivy-plugin.labels" . | nindent 4 }} +spec: +{{- include "trivy-plugin.podDisruptionBudget" . | indent 2 }} + selector: + matchLabels: + {{- include "trivy-plugin.selectorLabels" . | nindent 6 }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/templates/plugins/trivy/secret-role.yaml b/charts/policy-reporter/templates/plugins/trivy/secret-role.yaml new file mode 100644 index 00000000..bde89f85 --- /dev/null +++ b/charts/policy-reporter/templates/plugins/trivy/secret-role.yaml @@ -0,0 +1,17 @@ +{{- if .Values.plugin.trivy.enabled -}} +{{- if and .Values.plugin.trivy.serviceAccount.create .Values.plugin.trivy.rbac.enabled -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + {{- include "trivy-plugin.labels" . | nindent 4 }} + name: {{ include "trivy-plugin.fullname" . }}-secret-reader + namespace: {{ include "policyreporter.namespace" . }} +rules: +- apiGroups: [''] + resources: + - secrets + verbs: + - get +{{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/templates/plugins/trivy/secret-rolebinding.yaml b/charts/policy-reporter/templates/plugins/trivy/secret-rolebinding.yaml new file mode 100644 index 00000000..d81d76cb --- /dev/null +++ b/charts/policy-reporter/templates/plugins/trivy/secret-rolebinding.yaml @@ -0,0 +1,19 @@ +{{- if .Values.plugin.trivy.enabled -}} +{{- if and .Values.plugin.trivy.serviceAccount.create .Values.plugin.trivy.rbac.enabled -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "trivy-plugin.fullname" . }}-secret-reader + namespace: {{ include "policyreporter.namespace" . }} + labels: + {{- include "trivy-plugin.labels" . | nindent 4 }} +roleRef: + kind: Role + name: {{ include "trivy-plugin.fullname" . }}-secret-reader + apiGroup: rbac.authorization.k8s.io +subjects: +- kind: "ServiceAccount" + name: {{ include "trivy-plugin.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/templates/plugins/trivy/service.yaml b/charts/policy-reporter/templates/plugins/trivy/service.yaml new file mode 100644 index 00000000..22e19462 --- /dev/null +++ b/charts/policy-reporter/templates/plugins/trivy/service.yaml @@ -0,0 +1,25 @@ +{{- if .Values.plugin.trivy.enabled -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "trivy-plugin.fullname" . }} + namespace: {{ include "policyreporter.namespace" . }} + labels: + {{- include "trivy-plugin.labels" . | nindent 4 }} + {{- with .Values.plugin.trivy.service.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.plugin.trivy.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.plugin.trivy.service.type }} + ports: + - port: {{ .Values.plugin.trivy.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "trivy-plugin.selectorLabels" . | nindent 4 }} +{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/templates/plugins/trivy/serviceaccount.yaml b/charts/policy-reporter/templates/plugins/trivy/serviceaccount.yaml new file mode 100644 index 00000000..288e0201 --- /dev/null +++ b/charts/policy-reporter/templates/plugins/trivy/serviceaccount.yaml @@ -0,0 +1,16 @@ +{{- if .Values.plugin.trivy.enabled -}} +{{- if .Values.plugin.trivy.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "trivy-plugin.serviceAccountName" . }} + namespace: {{ include "policyreporter.namespace" . }} + labels: + {{- include "trivy-plugin.labels" . | nindent 4 }} + {{- with .Values.plugin.trivy.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.plugin.trivy.serviceAccount.automount }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/templates/role.yaml b/charts/policy-reporter/templates/role.yaml index 93473ccd..ba46dab3 100644 --- a/charts/policy-reporter/templates/role.yaml +++ b/charts/policy-reporter/templates/role.yaml @@ -1,4 +1,4 @@ -{{- if and .Values.rbac.enabled (or .Values.leaderElection.enabled (gt (int .Values.replicaCount) 1)) -}} +{{- if and .Values.rbac.enabled (gt (int .Values.replicaCount) 1) -}} apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: diff --git a/charts/policy-reporter/templates/rolebinding.yaml b/charts/policy-reporter/templates/rolebinding.yaml index 7690104a..6d2b1e4e 100644 --- a/charts/policy-reporter/templates/rolebinding.yaml +++ b/charts/policy-reporter/templates/rolebinding.yaml @@ -1,4 +1,4 @@ -{{- if and .Values.rbac.enabled (or .Values.leaderElection.enabled (gt (int .Values.replicaCount) 1)) -}} +{{- if and .Values.rbac.enabled (gt (int .Values.replicaCount) 1) -}} apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: diff --git a/charts/policy-reporter/templates/secret-role.yaml b/charts/policy-reporter/templates/secret-role.yaml index a6338164..0aa8a271 100644 --- a/charts/policy-reporter/templates/secret-role.yaml +++ b/charts/policy-reporter/templates/secret-role.yaml @@ -16,4 +16,6 @@ rules: - secrets verbs: - get + - list + - watch {{- end -}} diff --git a/charts/policy-reporter/templates/ui/_helpers.tpl b/charts/policy-reporter/templates/ui/_helpers.tpl new file mode 100644 index 00000000..bd424b57 --- /dev/null +++ b/charts/policy-reporter/templates/ui/_helpers.tpl @@ -0,0 +1,68 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "ui.name" -}} +{{ template "policyreporter.name" . }}-ui +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "ui.fullname" -}} +{{ template "policyreporter.fullname" . }}-ui +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "ui.chart" -}} +{{ template "policyreporter.chart" . }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "ui.labels" -}} +helm.sh/chart: {{ include "ui.chart" . }} +{{ include "ui.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- with .Values.global.labels }} +{{ toYaml . }} +{{- end -}} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "ui.selectorLabels" -}} +app.kubernetes.io/name: {{ include "ui.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "ui.serviceAccountName" -}} +{{- if .Values.plugin.kyverno.serviceAccount.create }} +{{- default (include "ui.fullname" .) .Values.plugin.kyverno.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.plugin.kyverno.serviceAccount.name }} +{{- end }} +{{- end }} + +{{- define "ui.podDisruptionBudget" -}} +{{- if and .Values.plugin.kyverno.ui.podDisruptionBudget.minAvailable .Values.plugin.kyverno.ui.podDisruptionBudget.maxUnavailable }} +{{- fail "Cannot set both" -}} +{{- end }} +{{- if not .Values.plugin.kyverno.ui.podDisruptionBudget.maxUnavailable }} +minAvailable: {{ default 1 .Values.plugin.kyverno.ui.podDisruptionBudget.minAvailable }} +{{- end }} +{{- if .Values.plugin.kyverno.ui.podDisruptionBudget.maxUnavailable }} +maxUnavailable: {{ .Values.plugin.kyverno.ui.podDisruptionBudget.maxUnavailable }} +{{- end }} +{{- end }} diff --git a/charts/policy-reporter/templates/ui/config-secret.yaml b/charts/policy-reporter/templates/ui/config-secret.yaml new file mode 100644 index 00000000..cd469930 --- /dev/null +++ b/charts/policy-reporter/templates/ui/config-secret.yaml @@ -0,0 +1,12 @@ +{{- if .Values.ui.enabled -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "ui.fullname" . }}-config + namespace: {{ include "policyreporter.namespace" . }} + labels: + {{- include "ui.labels" . | nindent 4 }} +type: Opaque +data: + config.yaml: {{ tpl (.Files.Get "configs/ui.tmpl") . | b64enc }} +{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/templates/ui/deployment.yaml b/charts/policy-reporter/templates/ui/deployment.yaml new file mode 100644 index 00000000..86fcd21a --- /dev/null +++ b/charts/policy-reporter/templates/ui/deployment.yaml @@ -0,0 +1,112 @@ +{{- if .Values.ui.enabled -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "ui.fullname" . }} + namespace: {{ include "policyreporter.namespace" . }} + labels: + {{- include "ui.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.ui.replicaCount }} + revisionHistoryLimit: {{ .Values.ui.revisionHistoryLimit }} + {{- with .Values.ui.updateStrategy }} + strategy: + {{- toYaml . | nindent 4 }} + {{- end }} + selector: + matchLabels: + {{- include "ui.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + checksum/secret: {{ include (print .Template.BasePath "/ui/config-secret.yaml") . | sha256sum | quote }} + checksum/cluster-secret: {{ include (print .Template.BasePath "/cluster-secret.yaml") . | sha256sum | quote }} + {{- with .Values.ui.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "ui.labels" . | nindent 8 }} + {{- with .Values.ui.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.ui.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "ui.serviceAccountName" . }} + {{- if .Values.ui.podSecurityContext }} + securityContext: + {{- toYaml .Values.ui.podSecurityContext | nindent 8 }} + {{- end }} + containers: + - name: policy-reporter-ui + {{- if .Values.ui.securityContext }} + securityContext: + {{- toYaml .Values.ui.securityContext | nindent 12 }} + {{- end }} + image: "{{ .Values.ui.image.registry }}/{{ .Values.ui.image.repository }}:{{ .Values.ui.image.tag }}" + imagePullPolicy: {{ .Values.ui.image.pullPolicy }} + args: + - run + - --config=/app/config.yaml + - --port={{ .Values.ui.server.port }} + ports: + - name: http + containerPort: {{ .Values.ui.server.port }} + protocol: TCP + livenessProbe: + httpGet: + path: /healthz + port: http + readinessProbe: + httpGet: + path: /healthz + port: http + resources: + {{- toYaml .Values.ui.resources | nindent 12 }} + volumeMounts: + - name: config-file + mountPath: /app/config.yaml + subPath: config.yaml + readOnly: true + - name: tmp + mountPath: /tmp + env: + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + {{- with .Values.ui.envVars }} + {{- . | toYaml | trim | nindent 10 }} + {{- end }} + {{- if .Values.ui.sidecarContainers }} + {{- range $name, $spec := .Values.ui.sidecarContainers }} + - name: {{ $name }} + {{- if kindIs "string" $spec }} + {{- tpl $spec $ | nindent 10 }} + {{- else }} + {{- toYaml $spec | nindent 10 }} + {{- end }} + {{- end }} + {{- end }} + volumes: + - name: config-file + secret: + secretName: {{ include "ui.fullname" . }}-config + optional: true + - name: tmp + emptyDir: {} + {{- with .Values.ui.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.ui.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.ui.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/templates/ui/extra-manifests.yaml b/charts/policy-reporter/templates/ui/extra-manifests.yaml new file mode 100644 index 00000000..cb1b0b91 --- /dev/null +++ b/charts/policy-reporter/templates/ui/extra-manifests.yaml @@ -0,0 +1,6 @@ +{{- if .Values.ui.enabled -}} +{{ range .Values.ui.extraManifests }} +--- +{{ tpl . $ }} +{{ end }} +{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/charts/ui/templates/ingress.yaml b/charts/policy-reporter/templates/ui/ingress.yaml similarity index 55% rename from charts/policy-reporter/charts/ui/templates/ingress.yaml rename to charts/policy-reporter/templates/ui/ingress.yaml index bcd45441..0df539a9 100644 --- a/charts/policy-reporter/charts/ui/templates/ingress.yaml +++ b/charts/policy-reporter/templates/ui/ingress.yaml @@ -1,9 +1,10 @@ -{{- if .Values.ingress.enabled -}} +{{- if .Values.ui.enabled -}} +{{- if .Values.ui.ingress.enabled -}} {{- $fullName := include "ui.fullname" . -}} -{{- $svcPort := .Values.ingress.port | default .Values.service.port -}} -{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} - {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} - {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} +{{- $svcPort := .Values.ui.ingress.port | default .Values.ui.service.port -}} +{{- if and .Values.ui.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ui.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ui.ingress.annotations "kubernetes.io/ingress.class" .Values.ui.ingress.className}} {{- end }} {{- end }} {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} @@ -16,33 +17,26 @@ apiVersion: extensions/v1beta1 kind: Ingress metadata: name: {{ $fullName }} - namespace: {{ include "ui.namespace" . }} + namespace: {{ include "policyreporter.namespace" . }} labels: {{- include "ui.labels" . | nindent 4 }} - {{- if .Values.ingress.labels }} - {{- with .Values.ingress.labels }} + {{- with .Values.ui.ingress.labels }} {{- toYaml . | nindent 4 }} {{- end }} - {{- end }} - {{- if or .Values.annotations .Values.ingress.annotations }} + {{- with .Values.ui.ingress.annotations }} annotations: - {{- with .Values.ingress.annotations }} {{- toYaml . | nindent 4 }} {{- end }} - {{- with .Values.annotations }} - {{- toYaml .Values.annotations | nindent 4 }} - {{- end }} - {{- end }} spec: - {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} - ingressClassName: {{ .Values.ingress.className }} + {{- if and .Values.ui.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ui.ingress.className }} {{- end }} - {{- if .Values.ingress.tls }} + {{- if .Values.ui.ingress.tls }} tls: - {{- toYaml .Values.ingress.tls | nindent 4 }} + {{- toYaml .Values.ui.ingress.tls | nindent 4 }} {{- end }} rules: - {{- range .Values.ingress.hosts }} + {{- range .Values.ui.ingress.hosts }} - host: {{ .host | quote }} http: paths: @@ -63,4 +57,5 @@ spec: {{- end }} {{- end }} {{- end }} +{{- end }} {{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/templates/ui/networkpolicy.yaml b/charts/policy-reporter/templates/ui/networkpolicy.yaml new file mode 100644 index 00000000..9278f782 --- /dev/null +++ b/charts/policy-reporter/templates/ui/networkpolicy.yaml @@ -0,0 +1,53 @@ +{{- if .Values.ui.enabled -}} +{{- if .Values.ui.networkPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + labels: {{- include "ui.labels" . | nindent 4 }} + name: {{ include "ui.fullname" . }} + namespace: {{ include "policyreporter.namespace" . }} +spec: + podSelector: + matchLabels: {{- include "ui.selectorLabels" . | nindent 6 }} + policyTypes: + - Ingress + - Egress + ingress: + - from: + ports: + - protocol: TCP + port: {{ .Values.ui.service.port }} + {{- with .Values.ui.networkPolicy.ingress }} + {{- toYaml . | nindent 2 }} + {{- end }} + egress: + - to: + - podSelector: + matchLabels: + {{- include "policyreporter.selectorLabels" . | nindent 10 }} + ports: + - protocol: TCP + port: {{ .Values.service.port }} + {{- if or .Values.plugin.kyverno.enabled }} + - to: + - podSelector: + matchLabels: + {{- include "kyverno-plugin.selectorLabels" . | nindent 10 }} + ports: + - protocol: TCP + port: {{ .Values.plugin.kyverno.service.port }} + {{- end }} + {{- if or .Values.plugin.trivy.enabled }} + - to: + - podSelector: + matchLabels: + {{- include "trivy-plugin.selectorLabels" . | nindent 10 }} + ports: + - protocol: TCP + port: {{ .Values.plugin.trivy.service.port }} + {{- end }} + {{- with .Values.networkPolicy.egress }} + {{- toYaml . | nindent 2 }} + {{- end }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/charts/ui/templates/poddisruptionbudget.yaml b/charts/policy-reporter/templates/ui/poddisruptionbudget.yaml similarity index 52% rename from charts/policy-reporter/charts/ui/templates/poddisruptionbudget.yaml rename to charts/policy-reporter/templates/ui/poddisruptionbudget.yaml index b5d8dde1..55d73362 100644 --- a/charts/policy-reporter/charts/ui/templates/poddisruptionbudget.yaml +++ b/charts/policy-reporter/templates/ui/poddisruptionbudget.yaml @@ -1,4 +1,5 @@ -{{- if (gt (int .Values.replicaCount) 1) }} +{{- if .Values.ui.enabled -}} +{{- if (gt (int .Values.ui.replicaCount) 1) }} {{- if .Capabilities.APIVersions.Has "policy/v1/PodDisruptionBudget" }} apiVersion: policy/v1 {{- else }} @@ -6,17 +7,14 @@ apiVersion: policy/v1beta1 {{- end }} kind: PodDisruptionBudget metadata: - name: {{ template "ui.fullname" . }} - namespace: {{ include "ui.namespace" . }} + name: {{ include "ui.fullname" . }} + namespace: {{ include "policyreporter.namespace" . }} labels: {{- include "ui.labels" . | nindent 4 }} - {{- if .Values.annotations }} - annotations: - {{- toYaml .Values.annotations | nindent 4 }} - {{- end }} spec: -{{- include "policyreporter.podDisruptionBudget" . | indent 2 }} +{{- include "ui.podDisruptionBudget" . | indent 2 }} selector: matchLabels: {{- include "ui.selectorLabels" . | nindent 6 }} +{{- end }} {{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/charts/ui/templates/secret-role.yaml b/charts/policy-reporter/templates/ui/secret-role.yaml similarity index 50% rename from charts/policy-reporter/charts/ui/templates/secret-role.yaml rename to charts/policy-reporter/templates/ui/secret-role.yaml index 7df123f8..faf39821 100644 --- a/charts/policy-reporter/charts/ui/templates/secret-role.yaml +++ b/charts/policy-reporter/templates/ui/secret-role.yaml @@ -1,19 +1,17 @@ -{{- if and .Values.serviceAccount.create .Values.rbac.enabled -}} +{{- if .Values.ui.enabled -}} +{{- if and .Values.ui.serviceAccount.create .Values.ui.rbac.enabled -}} apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: - {{- if .Values.annotations }} - annotations: - {{- toYaml .Values.annotations | nindent 4 }} - {{- end }} labels: {{- include "ui.labels" . | nindent 4 }} name: {{ include "ui.fullname" . }}-secret-reader - namespace: {{ include "ui.namespace" . }} + namespace: {{ include "policyreporter.namespace" . }} rules: - apiGroups: [''] resources: - secrets verbs: - get -{{- end -}} \ No newline at end of file +{{- end -}} +{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/charts/ui/templates/secret-rolebinding.yaml b/charts/policy-reporter/templates/ui/secret-rolebinding.yaml similarity index 59% rename from charts/policy-reporter/charts/ui/templates/secret-rolebinding.yaml rename to charts/policy-reporter/templates/ui/secret-rolebinding.yaml index 980799be..ccae6b88 100644 --- a/charts/policy-reporter/charts/ui/templates/secret-rolebinding.yaml +++ b/charts/policy-reporter/templates/ui/secret-rolebinding.yaml @@ -1,13 +1,10 @@ -{{- if and .Values.serviceAccount.create .Values.rbac.enabled -}} +{{- if .Values.ui.enabled -}} +{{- if and .Values.ui.serviceAccount.create .Values.rbac.enabled -}} apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: {{ include "ui.fullname" . }}-secret-reader - namespace: {{ include "ui.namespace" . }} - {{- if .Values.annotations }} - annotations: - {{- toYaml .Values.annotations | nindent 4 }} - {{- end }} + namespace: {{ include "policyreporter.namespace" . }} labels: {{- include "ui.labels" . | nindent 4 }} roleRef: @@ -17,5 +14,6 @@ roleRef: subjects: - kind: "ServiceAccount" name: {{ include "ui.serviceAccountName" . }} - namespace: {{ include "ui.namespace" . }} + namespace: {{ .Release.Namespace }} {{- end -}} +{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/templates/ui/service.yaml b/charts/policy-reporter/templates/ui/service.yaml new file mode 100644 index 00000000..9cc154be --- /dev/null +++ b/charts/policy-reporter/templates/ui/service.yaml @@ -0,0 +1,28 @@ +{{- if .Values.ui.enabled -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "ui.fullname" . }} + namespace: {{ include "policyreporter.namespace" . }} + labels: + {{- include "ui.labels" . | nindent 4 }} + {{- with .Values.ui.service.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.ui.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.ui.service.type }} + ports: + - port: {{ .Values.ui.service.port }} + targetPort: http + protocol: TCP + name: http + {{- if .Values.ui.service.additionalPorts }} + {{ toYaml .Values.ui.service.additionalPorts | indent 4 }} + {{- end }} + selector: + {{- include "ui.selectorLabels" . | nindent 4 }} +{{- end }} \ No newline at end of file diff --git a/charts/policy-reporter/templates/ui/serviceaccount.yaml b/charts/policy-reporter/templates/ui/serviceaccount.yaml new file mode 100644 index 00000000..2cb12420 --- /dev/null +++ b/charts/policy-reporter/templates/ui/serviceaccount.yaml @@ -0,0 +1,16 @@ +{{- if .Values.ui.enabled -}} +{{- if .Values.ui.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "ui.serviceAccountName" . }} + namespace: {{ include "policyreporter.namespace" . }} + labels: + {{- include "ui.labels" . | nindent 4 }} + {{- with .Values.ui.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.ui.serviceAccount.automount }} +{{- end }} +{{- end }} diff --git a/charts/policy-reporter/values.yaml b/charts/policy-reporter/values.yaml index 6d4beb91..ec2d31bd 100644 --- a/charts/policy-reporter/values.yaml +++ b/charts/policy-reporter/values.yaml @@ -1,60 +1,76 @@ -# Override the chart name used for all resources +# -- Override the chart name used for all resources nameOverride: "" -image: - registry: ghcr.io - repository: kyverno/policy-reporter - pullPolicy: IfNotPresent - tag: 2.20.2 +# -- Overwrite the fullname of all resources +fullnameOverride: "policy-reporter" +# -- Overwrite the namespace of all resources +namespaceOverride: "" + +image: + # -- (string) Image registry + registry: ghcr.io + # -- (string) Image repository + repository: kyverno/policy-reporter + # -- (string) Image pullPolicy + pullPolicy: IfNotPresent + # -- (string) Image tag + tag: 12da466 + +# -- Image pullSecrets imagePullSecrets: [] +# -- Deployment priorityClassName priorityClassName: "" +# -- Deployment replica count replicaCount: 1 +# -- The number of revisions to keep revisionHistoryLimit: 10 -deploymentStrategy: {} +# -- Deployment strategy +updateStrategy: {} # rollingUpdate: # maxSurge: 25% # maxUnavailable: 25% # type: RollingUpdate -# When using a custom port together with the PolicyReporter UI -# the port has also to be changed in the UI subchart as well because it can't access the parent values. -# You can change the port under `ui.policyReporter.port` +# -- Container port port: name: http number: 8080 -# Key/value pairs that are attached to all resources. +# -- Key/value pairs that are attached to all resources. annotations: {} -# Create cluster role policies rbac: + # -- Create RBAC resources enabled: true serviceAccount: - # Specifies whether a service account should be created + # -- Create ServiceAccount create: true - # Annotations to add to the service account + # -- Enable ServiceAccount automaount + automount: true + # -- Annotations for the ServiceAccount annotations: {} - # The name of the service account to use. - # If not set and create is true, a name is generated using the fullname template + # -- The ServiceAccount name name: "" service: + # -- Create Service enabled: true - ## configuration of service - # key/value - annotations: {} - # key/value - labels: {} + # -- Service type type: ClusterIP - # integer number. This is port for service + # -- Service port port: 8080 + # -- Service annotations + annotations: {} + # -- Service labels + labels: {} +# -- Security context for the pod podSecurityContext: fsGroup: 1234 @@ -70,20 +86,14 @@ securityContext: seccompProfile: type: RuntimeDefault -# Key/value pairs that are attached to pods. -podAnnotations: {} + # -- Additional annotations to add to each pod + podAnnotations: {} -# Key/value pairs that are attached to pods. -podLabels: {} - -# Allow additional env variables to be added -envVars: [] + # -- Additional labels to add to each pod + podLabels: {} +# -- Resource constraints resources: {} - # We usually recommend not to specify default resources and to leave this as a conscious - # choice for the user. This also increases chances charts run on environments with little - # resources, such as Minikube. If you do want to specify resources, uncomment the following - # lines, adjust them as necessary, and remove the curly braces after 'resources:'. # limits: # memory: 100Mi # cpu: 10m @@ -91,11 +101,10 @@ resources: {} # memory: 75Mi # cpu: 5m -# Enable a NetworkPolicy for this chart. Useful on clusters where Network Policies are -# used and configured in a default-deny fashion. networkPolicy: + # -- Create NetworkPolicy enabled: false - # Kubernetes API Server + # -- Egress rule to allowe Kubernetes API Server access egress: - to: ports: @@ -103,158 +112,163 @@ networkPolicy: port: 6443 ingress: [] -## Set to true to enable ingress record generation -# ref to: https://kubernetes.io/docs/concepts/services-networking/ingress/ ingress: + # -- Create Ingress + # This ingress exposes the policy-reporter core app. enabled: false + # -- Ingress className className: "" - # key/value + # -- Labels for the Ingress labels: {} - # key/value + # -- Annotations for the Ingress annotations: {} # kubernetes.io/ingress.class: nginx # kubernetes.io/tls-acme: "true" + # -- Ingress host list hosts: - - host: chart-example.local - paths: [] + # - host: chart-example.local + # paths: [] + # -- Ingress tls list tls: [] # - secretName: chart-example-tls # hosts: # - chart-example.local logging: - encoding: console # possible encodings are console and json - logLevel: 0 # default info - development: false # more human readable structure, enables stacktraces and removes log sampling + # -- Enables server access logging + server: false + # -- Log encoding + # possible encodings are console and json + encoding: console + # -- Log level + # default info + logLevel: 0 -api: - logging: false # enable debug API access logging, sets logLevel to debug - -# REST API rest: + # -- Enables the REST API enabled: false -# Prometheus Metrics API metrics: + # -- Enables Prometheus Metrics enabled: false - mode: detailed # available modes are detailed, simple and custom - customLabels: [] # only used for custom mode. Supported fields are: ["namespace", "rule", "policy", "report" // PolicyReport name, "kind" // resource kind, "name" // resource name, "status", "severity", "category", "source"] -# filter: + # -- Metric Mode allowes to customize labels + # Allowed values: detailed, simple, custom + mode: detailed + # -- List of used labels in custom mode + # Supported fields are: ["namespace", "rule", "policy", "report" // PolicyReport name, "kind" // resource kind, "name" // resource name, "status", "severity", "category", "source"] + customLabels: [] + # -- Filter results to reduce cardinality + filter: {} # sources: # exclude: ["Trivy CIS Kube Bench"] # status: # exclude: ["pass", "skip"] profiling: + # -- Enable profiling with pprof enabled: false -# amount of queue workers for PolicyReport resource processing +# -- Amount of queue workers for PolicyReport resource processing worker: 5 -# Filter PolicyReport resources to process -reportFilter: - namespaces: - # Process only PolicyReport resources from an included namespace, wildcards are supported - include: [] - # Ignore all PolicyReport resources from a excluded namespace, wildcards are supported - # exclude will be ignored if an include filter exists - exclude: [] - clusterReports: - # Disable the processing of ClusterPolicyReports - disabled: false +# -- Filter PolicyReport resources to process +reportFilter: {} + # # -- Filter reports based on an namespace allow- or disallow list, wildcards are supported + # namespaces: + # include: [] + # exclude: [] + # # -- Disable the processing of ClusterPolicyReports + # disableClusterReports: false -# customize source specific logic like result ID generation -sourceConfig: {} -# sourcename: -# customID: +# -- Customize source specific logic like result ID generation +sourceConfig: [] +# - selector: +# source: kyverno +# customId: # enabled: true # fields: ["resource", "policy", "rule", "category", "result", "message"] -# Settings for the Policy Reporter UI subchart (see subchart's values.yaml) -ui: - enabled: false - -kyvernoPlugin: - enabled: false - -# Settings for the monitoring subchart -monitoring: - enabled: false - -database: - # Database Type, supported: mysql, postgres, mariadb - type: "" - database: "" # Database Name - username: "" - password: "" - host: "" - enableSSL: false - # instead of configure the individual values you can also provide an DSN string - # example postgres: postgres://postgres:password@localhost:5432/postgres?sslmode=disable - # example mysql: root:password@tcp(localhost:3306)/test?tls=false - dsn: "" - # configure an existing secret as source for your values - # supported fields: username, password, host, dsn, database - secretRef: "" - # use an mounted secret as source for your values, required the information in JSON format - # supported fields: username, password, host, dsn, database - mountedSecret: "" +# Source based PolicyReport filter +sourceFilters: + - selector: + # -- select PolicyReport by source + source: kyverno + # -- Filter out PolicyReports of controlled Pods and Jobs, only works for PolicyReport with scope resource + uncontrolledOnly: true + # -- Filter out ClusterPolicyReports + disableClusterReports: false + # -- Filter out PolicyReports based on the scope resource kind + kinds: + exclude: [ReplicaSet] global: - # available plugins - plugins: - # enable kyverno for Policy Reporter UI and monitoring - kyverno: false - # The name of service policy-report. Defaults to ReleaseName. - backend: "" - # overwrite the fullname of all resources including subcharts - fullnameOverride: "" - # configure the namespace of all resources including subcharts - namespace: "" - # additional labels added on each resource + # -- additional labels added on each resource labels: {} - # basicAuth for APIs and metrics - basicAuth: - # HTTP BasicAuth username - username: "" - # HTTP BasicAuth password - password: "" - # read credentials from secret - secretRef: "" + +# basicAuth for APIs and metrics +basicAuth: + # -- HTTP BasicAuth username + username: "" + # -- HTTP BasicAuth password + password: "" + # -- (optional) Secret reference to get username and/or password from + secretRef: "" emailReports: - clusterName: "" # (optional) - displayed in the email report if configured - titlePrefix: "Report" # title prefix in the email subject + # -- (optional) - Displayed in the email report if configured + clusterName: "" + # -- Title prefix in the email subject + titlePrefix: "Report" + # -- Resource constraints for the created CronJobs + resources: {} smtp: - secret: "" # (optional) secret name to provide the complete or partial SMTP configuration + # -- (optional) Secret reference to provide the complete or partial SMTP configuration + secret: "" + # -- SMTP Server Host host: "" + # -- SMTP Server Port port: 465 + # -- SMTP Username username: "" + # -- SMTP Password password: "" - from: "" # displayed from email address - encryption: "" # default is none, supports ssl/tls and starttls + # -- Displayed from email address + from: "" + # -- SMTP Encryption + # Default is none, supports ssl/tls and starttls + encryption: "" + # -- Skip SMTP TLS verification skipTLS: false + # -- SMTP Server Certificate file path certificate: "" - # basic summary report summary: + # -- Enable Summary E-Mail reports enabled: false - schedule: "0 8 * * *" # CronJob schedule defines when the report will be send - activeDeadlineSeconds: 300 # timeout in seconds - backoffLimit: 3 # retry counter + # -- CronJob schedule + schedule: "0 8 * * *" + # -- CronJob activeDeadlineSeconds + activeDeadlineSeconds: 300 + # -- CronJob backoffLimit + backoffLimit: 3 + # -- CronJob ttlSecondsAfterFinished ttlSecondsAfterFinished: 0 - restartPolicy: Never # pod restart policy - - to: [] # list of receiver email addresses - filter: {} # optional filters - # disableClusterReports: false # remove ClusterPolicyResults from Reports + # -- CronJob restartPolicy + restartPolicy: Never + # -- List of receiver email addresses + to: [] + # -- (optional) Report filter + filter: {} + # # remove ClusterPolicyResults from Reports + # disableClusterReports: false # namespaces: # include: [] # exclude: [] # sources: # include: [] # exclude: [] - channels: [] # (optional) channels can be used to to send only a subset of namespaces / sources to dedicated email addresses channels: [] # (optional) channels can be used to to send only a subset of namespaces / sources to dedicated email addresses + # -- (optional) Channels can be used to to send only a subset of namespaces / sources to dedicated email addresses + channels: [] # - to: ['team-a@company.org'] # filter: # disableClusterReports: true @@ -262,17 +276,24 @@ emailReports: # include: ['team-a-*'] # sources: # include: ['Kyverno'] - # violation summary report + violations: + # -- Enable Violation Summary E-Mail reports enabled: false - schedule: "0 8 * * *" # CronJob schedule defines when the report will be send - activeDeadlineSeconds: 300 # timeout in seconds - backoffLimit: 3 # retry counter + # -- CronJob schedule + schedule: "0 8 * * *" + # -- CronJob activeDeadlineSeconds + activeDeadlineSeconds: 300 + # -- CronJob backoffLimit + backoffLimit: 3 + # -- CronJob ttlSecondsAfterFinished ttlSecondsAfterFinished: 0 - restartPolicy: Never # pod restart policy - - to: [] # list of receiver email addresses - filter: {} # optional filters + # -- CronJob restartPolicy + restartPolicy: Never + # -- List of receiver email addresses + to: [] + # -- (optional) Report filter + filter: {} # disableClusterReports: false # remove ClusterPolicyResults from Reports # namespaces: # include: [] @@ -280,7 +301,8 @@ emailReports: # sources: # include: [] # exclude: [] - channels: [] # (optional) channels can be used to to send only a subset of namespaces / sources to dedicated email addresses channels: [] # (optional) channels can be used to to send only a subset of namespaces / sources to dedicated email addresses + # -- (optional) Channels can be used to to send only a subset of namespaces / sources to dedicated email addresses + channels: [] # - to: ['team-a@company.org'] # filter: # disableClusterReports: true @@ -288,52 +310,45 @@ emailReports: # include: ['team-a-*'] # sources: # include: ['Kyverno'] - resources: {} - # limits: - # memory: 100Mi - # cpu: 10m - # requests: - # memory: 75Mi - # cpu: 5m -# Reference a configuration which already exists instead of creating one existingTargetConfig: + # -- Use an already existing configuration enabled: false - # Name of the secret with the config + # -- Name of the secret with the config name: "" - # subPath within the secret (defaults to config.yaml) + # -- SubPath within the secret (defaults to config.yaml) subPath: "" -# Supported targets for new PolicyReport Results target: loki: - # loki host address + # -- Host Address host: "" - # path to your custom certificate - # can be added under extraVolumes - certificate: "" - # skip TLS verification if necessary - skipTLS: false - # receive the host from an existing secret instead - secretRef: "" - # Mounted secret path by Secrets Controller, secret should be in json format - mountedSecret: "" - # loki api path, defaults to "/api/prom/push" (deprecated) + # -- Loki API, defaults to "/loki/api/v1/push" path: "" - # minimum priority "" < info < warning < critical < error - minimumPriority: "" - # list of sources which should send to loki + # -- Server Certificate file path + # Can be added under extraVolumes + certificate: "" + # -- Skip TLS verification + skipTLS: false + # -- Read configuration from an already existing Secret + secretRef: "" + # -- Mounted secret path by Secrets Controller, secret should be in json format + mountedSecret: "" + # -- Minimum severity: "" < info < low < medium < high < critical + minimumSeverity: "" + # -- List of sources which should send sources: [] - # Skip already existing PolicyReportResults on startup + # -- Skip already existing PolicyReportResults on startup skipExistingOnStartup: true - # Added as additional labels to each Loki event - customLabels: {} - # Additional custom HTTP Headers + # -- Added as additional labels + customFields: {} + # -- Additional HTTP Headers headers: {} - # HTTP BasicAuth credentials for Loki + # -- HTTP BasicAuth username username: "" + # -- HTTP BasicAuth password password: "" - # Filter Results which should send to this target by report labels, namespaces, priorities or policies + # -- Filter Results which should send to this target # Wildcars for namespaces and policies are supported, you can either define exclude or include values # Filters are available for all targets except the UI filter: {} @@ -343,6 +358,7 @@ target: # exclude: ["debug", "info", "error"] # labels: # include: ["app", "owner:team-a", "monitoring:*"] + # -- List of channels to route results to different configurations channels: [] # - host: "http://loki.loki-stack:3100" # sources: [] @@ -356,63 +372,68 @@ target: # . include: ["app", "owner:team-b"] elasticsearch: - # elasticsearch host address + # -- Host address host: "" - # path to your custom certificate - # can be added under extraVolumes + # -- Server Certificate file path + # Can be added under extraVolumes certificate: "" - # skip TLS verification if necessary + # -- Skip TLS verification skipTLS: false - # elasticsearch index (default: policy-reporter) - index: "" - # elasticsearch username für HTTP Basic Auth - username: "" - # elasticsearch password für HTTP Basic Auth - password: "" - # elasticsearch apiKey für apiKey authentication - apiKey: "" - # receive the host, username and/or password,apiKey from an existing secret instead - secretRef: "" - # Mounted secret path by Secrets Controller, secret should be in json format - mountedSecret: "" - # elasticsearch index rotation and index suffix - # possible values: daily, monthly, annually, none (default: daily) - rotation: "" - # minimum priority "" < info < warning < critical < error - minimumPriority: "" - # list of sources which should send to elasticsearch - sources: [] - # Skip already existing PolicyReportResults on startup - skipExistingOnStartup: true + # -- Elasticsearch index (default: policy-reporter) + index: "policy-reporter" + # -- Elasticsearch index rotation and index suffix + # Possible values: daily, monthly, annually, none (default: daily) + rotation: "daily" + # -- Enables Elasticsearch typless API # https://www.elastic.co/blog/moving-from-types-to-typeless-apis-in-elasticsearch-7-0 keeping as false for retrocompatibility. typelessApi: false - # Added as additional properties to each elasticsearch event + # -- HTTP BasicAuth username + username: "" + # -- HTTP BasicAuth password + password: "" + # -- Elasticsearch API Key for api key authentication + apiKey: "" + # -- Read configuration from an already existing Secret + secretRef: "" + # -- Mounted secret path by Secrets Controller, secret should be in json format + mountedSecret: "" + # -- Minimum severity: "" < info < low < medium < high < critical + minimumSeverity: "" + # -- List of sources which should send + sources: [] + # -- Skip already existing PolicyReportResults on startup + skipExistingOnStartup: true + # -- Added as additional labels customFields: {} - # filter results send by namespaces, policies and priorities + # -- Filter Results which should send to this target + # Wildcars for namespaces and policies are supported, you can either define exclude or include values + # Filters are available for all targets except the UI filter: {} - # add additional elasticsearch channels with different configurations and filters + # -- List of channels to route results to different configurations channels: [] slack: - # slack app webhook address + # -- Webhook Address webhook: "" - # slack channel + # -- Slack Channel channel: "" - # receive the webhook from an existing secret instead + # -- Read configuration from an already existing Secret secretRef: "" - # Mounted secret path by Secrets Controller, secret should be in json format + # -- Mounted secret path by Secrets Controller, secret should be in json format mountedSecret: "" - # minimum priority "" < info < warning < critical < error - minimumPriority: "" - # list of sources which should send to slack + # -- Minimum severity: "" < info < low < medium < high < critical + minimumSeverity: "" + # -- List of sources which should send sources: [] - # Skip already existing PolicyReportResults on startup + # -- Skip already existing PolicyReportResults on startup skipExistingOnStartup: true - # Added as additional fields to each Slack event + # -- Added as additional labels customFields: {} - # filter results send by namespaces, policies and priorities + # -- Filter Results which should send to this target + # Wildcars for namespaces and policies are supported, you can either define exclude or include values + # Filters are available for all targets except the UI filter: {} - # add additional slack channels with different configurations and filters + # -- List of channels to route results to different configurations channels: [] # - webhook: "https://slack.webhook1" # channel: "" @@ -426,293 +447,313 @@ target: # reportLabels: # . include: ["app", "owner:team-b"] # - webhook: "https://slack.webhook2" -# minimumPriority: "warning" +# minimumSeverity: "warning" # filter: # namespaces: # include: ["team-a-*"] discord: - # discord app webhook address + # -- Webhook Address webhook: "" - # receive the webhook from an existing secret instead + # -- Read configuration from an already existing Secret secretRef: "" - # Mounted secret path by Secrets Controller, secret should be in json format + # -- Mounted secret path by Secrets Controller, secret should be in json format mountedSecret: "" - # minimum priority "" < info < warning < critical < error - minimumPriority: "" - # list of sources which should send to discord + # -- Minimum severity: "" < info < low < medium < high < critical + minimumSeverity: "" + # -- List of sources which should send sources: [] - # Skip already existing PolicyReportResults on startup + # -- Skip already existing PolicyReportResults on startup skipExistingOnStartup: true - # filter results send by namespaces, policies and priorities + # -- Added as additional labels + customFields: {} + # -- Filter Results which should send to this target + # Wildcars for namespaces and policies are supported, you can either define exclude or include values + # Filters are available for all targets except the UI filter: {} - # add additional discord channels with different configurations and filters + # -- List of channels to route results to different configurations channels: [] teams: - # teams webhook address + # -- Webhook Address webhook: "" - # receive the webhook from an existing secret instead + # -- Read configuration from an already existing Secret secretRef: "" - # Mounted secret path by Secrets Controller, secret should be in json format + # -- Mounted secret path by Secrets Controller, secret should be in json format mountedSecret: "" - # path to your custom certificate - # can be added under extraVolumes - certificate: "" - # skip TLS verification if necessary - skipTLS: false - # minimum priority "" < info < warning < critical < error - minimumPriority: "" - # list of sources which should send to teams + # -- Minimum severity: "" < info < low < medium < high < critical + minimumSeverity: "" + # -- List of sources which should send sources: [] - # Skip already existing PolicyReportResults on startup + # -- Skip already existing PolicyReportResults on startup skipExistingOnStartup: true - # filter results send by namespaces, policies and priorities + # -- Added as additional labels + customFields: {} + # -- Filter Results which should send to this target + # Wildcars for namespaces and policies are supported, you can either define exclude or include values + # Filters are available for all targets except the UI filter: {} - # add additional teams channels with different configurations and filters + # -- List of channels to route results to different configurations channels: [] - ui: - # ui host address - host: "" - # path to your custom certificate - # can be added under extraVolumes - certificate: "" - # skip TLS verification if necessary - skipTLS: false - # minimum priority "" < info < warning < critical < error - minimumPriority: "warning" - # list of sources which should send to the UI Log - sources: [] - # Skip already existing PolicyReportResults on startup - skipExistingOnStartup: true - webhook: - # webhook host address + # -- Webhook Address host: "" - # path to your custom certificate - # can be added under extraVolumes - certificate: "" - # skip TLS verification if necessary - skipTLS: false - # receive the host and/or token from an existing secret, the token is added as Authorization header - secretRef: "" - # Mounted secret path by Secrets Controller, secret should be in json format - mountedSecret: "" - # additional http headers + # -- Additional HTTP Headers headers: {} - # minimum priority "" < info < warning < critical < error - minimumPriority: "" - # list of sources which should send to the UI Log + # -- Read configuration from an already existing Secret + secretRef: "" + # -- Mounted secret path by Secrets Controller, secret should be in json format + mountedSecret: "" + # -- Minimum severity: "" < info < low < medium < high < critical + minimumSeverity: "" + # -- List of sources which should send sources: [] - # Skip already existing PolicyReportResults on startup + # -- Skip already existing PolicyReportResults on startup skipExistingOnStartup: true - # Added as additional properties to each webhook event + # -- Added as additional labels customFields: {} - # filter results send by namespaces, policies and priorities + # -- Filter Results which should send to this target + # Wildcars for namespaces and policies are supported, you can either define exclude or include values + # Filters are available for all targets except the UI filter: {} - # add additional webhook channels with different configurations and filters + # -- List of channels to route results to different configurations channels: [] telegram: - # telegram bot token + # -- Telegram bot token token: "" - # telegram chat id - chatID: "" - # optional telegram proxy host + # -- Telegram chat id + chatId: "" + # -- (optional) Telegram proxy host host: "" - # path to your custom certificate - # can be added under extraVolumes - certificate: "" - # skip TLS verification if necessary - skipTLS: false - # receive the host and/or token from an existing secret, the token is added as Authorization header - secretRef: "" - # Mounted secret path by Secrets Controller, secret should be in json format - mountedSecret: "" - # additional http headers + # -- Additional HTTP Headers headers: {} - # minimum priority "" < info < warning < critical < error - minimumPriority: "" - # list of sources which should send to telegram + # -- Read configuration from an already existing Secret + secretRef: "" + # -- Mounted secret path by Secrets Controller, secret should be in json format + mountedSecret: "" + # -- Minimum severity: "" < info < low < medium < high < critical + minimumSeverity: "" + # -- List of sources which should send sources: [] - # Skip already existing PolicyReportResults on startup + # -- Skip already existing PolicyReportResults on startup skipExistingOnStartup: true - # Added as additional properties to each notification + # -- Added as additional labels customFields: {} - # filter results send by namespaces, policies and priorities + # -- Filter Results which should send to this target + # Wildcars for namespaces and policies are supported, you can either define exclude or include values + # Filters are available for all targets except the UI filter: {} - # add additional telegram channels with different configurations and filters + # -- List of channels to route results to different configurations channels: [] googleChat: - # GoogleChat webhook + # -- Webhook Address webhook: "" - # path to your custom certificate - # can be added under extraVolumes - certificate: "" - # skip TLS verification if necessary - skipTLS: false - # receive the host and/or token from an existing secret, the token is added as Authorization header - secretRef: "" - # Mounted secret path by Secrets Controller, secret should be in json format - mountedSecret: "" - # additional http headers + # -- Additional HTTP Headers headers: {} - # minimum priority "" < info < warning < critical < error - minimumPriority: "" - # list of sources which should send to telegram + # -- Read configuration from an already existing Secret + secretRef: "" + # -- Mounted secret path by Secrets Controller, secret should be in json format + mountedSecret: "" + # -- Minimum severity: "" < info < low < medium < high < critical + minimumSeverity: "" + # -- List of sources which should send sources: [] - # Skip already existing PolicyReportResults on startup + # -- Skip already existing PolicyReportResults on startup skipExistingOnStartup: true - # Added as additional properties to each notification + # -- Added as additional labels customFields: {} - # filter results send by namespaces, policies and priorities + # -- Filter Results which should send to this target + # Wildcars for namespaces and policies are supported, you can either define exclude or include values + # Filters are available for all targets except the UI filter: {} - # add additional telegram channels with different configurations and filters + # -- List of channels to route results to different configurations channels: [] + # Authentication via PodIdentity or WebIdentity are also supported s3: - # S3 access key - accessKeyID: "" - # S3 secret access key + # -- (optional) S3 Access key + accessKeyId: "" + # -- (optional) S3 SecretAccess key secretAccessKey: "" - # receive the accessKeyID and/or secretAccessKey from an existing secret instead - secretRef: "" - # Mounted secret path by Secrets Controller, secret should be in json format - mountedSecret: "" - # S3 storage region + # -- (optional) S3 Storage region region: "" - # S3 storage endpoint + # -- (optional) S3 Storage endpoint endpoint: "" - # S3 storage, bucket name + # -- (required) S3 Storage bucket name bucket: "" - # S3 storage to use an S3 Bucket Key for object encryption with SSE-KMS + # -- S3 Storage to use an S3 Bucket Key for object encryption with SSE-KMS bucketKeyEnabled: false - # S3 storage KMS Key ID for object encryption with SSE-KMS + # -- S3 Storage KMS Key ID for object encryption with SSE-KMS kmsKeyId: "" - # S3 storage server-side encryption algorithm used when storing this object in Amazon S3, AES256, aws:kms + # -- S3 Storage server-side encryption algorithm used when storing this object in Amazon S3, AES256, aws:kms serverSideEncryption: "" - # S3 storage, force path style configuration + # -- S3 Storage, force path style configuration pathStyle: false - # name of prefix, keys will have format: s3:////YYYY-MM-DD/YYYY-MM-DDTHH:mm:ss.s+01:00.json + # -- Used prefix, keys will have format: s3:////YYYY-MM-DD/YYYY-MM-DDTHH:mm:ss.s+01:00.json prefix: "" - # minimum priority "" < info < warning < critical < error - minimumPriority: "" - # list of sources which should send to S3 + # -- Read configuration from an already existing Secret + secretRef: "" + # -- Mounted secret path by Secrets Controller, secret should be in json format + mountedSecret: "" + # -- Minimum severity: "" < info < low < medium < high < critical + minimumSeverity: "" + # -- List of sources which should send sources: [] - # Skip already existing PolicyReportResults on startup + # -- Skip already existing PolicyReportResults on startup skipExistingOnStartup: true - # Added as additional properties to each s3 event + # -- Added as additional labels customFields: {} - # filter results send by namespaces, policies and priorities + # -- Filter Results which should send to this target + # Wildcars for namespaces and policies are supported, you can either define exclude or include values + # Filters are available for all targets except the UI filter: {} - # add additional s3 channels with different configurations and filters + # -- List of channels to route results to different configurations channels: [] + # Authentication via PodIdentity or WebIdentity are also supported kinesis: - # AWS access key - accessKeyID: "" - # AWS secret access key + # -- (optional) Access key + accessKeyId: "" + # -- (optional) SecretAccess key secretAccessKey: "" - # receive the accessKeyID and/or secretAccessKey from an existing secret instead - secretRef: "" - # Mounted secret path by Secrets Controller, secret should be in json format - mountedSecret: "" - # AWS region + # -- (optional) Region region: "" - # AWS Kinesis endpoint + # -- (optional) Endpoint endpoint: "" - # AWS Kinesis stream name + # -- (required) StreamName streamName: "" - # minimum priority "" < info < warning < critical < error - minimumPriority: "" - # list of sources which should send to S3 + # -- Read configuration from an already existing Secret + secretRef: "" + # -- Mounted secret path by Secrets Controller, secret should be in json format + mountedSecret: "" + # -- Minimum severity: "" < info < low < medium < high < critical + minimumSeverity: "" + # -- List of sources which should send sources: [] - # Skip already existing PolicyReportResults on startup + # -- Skip already existing PolicyReportResults on startup skipExistingOnStartup: true - # Added as additional properties to each kinesis event + # -- Added as additional labels customFields: {} - # filter results send by namespaces, policies and priorities + # -- Filter Results which should send to this target + # Wildcars for namespaces and policies are supported, you can either define exclude or include values + # Filters are available for all targets except the UI filter: {} - # add additional s3 channels with different configurations and filters + # -- List of channels to route results to different configurations channels: [] + # Authentication via PodIdentity or WebIdentity are also supported securityHub: - # AWS access key - accessKeyID: "" - # AWS secret access key + # -- (optional) Access key + accessKeyId: "" + # -- (optional) SecretAccess key secretAccessKey: "" - # receive the accessKeyID and/or secretAccessKey from an existing secret instead - secretRef: "" - # Mounted secret path by Secrets Controller, secret should be in json format - mountedSecret: "" - # AWS region + # -- (optional) Region region: "" - # AWS SecurityHub endpoint (optional) + # -- (optional) Endpoint endpoint: "" - # AWS accountID - accountID: "" - # Used product name, defaults to "Polilcy Reporter" + # -- (required) AccountId + accountId: "" + # -- (optional) Used product name, defaults to "Polilcy Reporter" productName: "" - # minimum priority "" < info < warning < critical < error - minimumPriority: "" - # list of sources which should send to S3 - sources: [] - # Skip already existing PolicyReportResults on startup - skipExistingOnStartup: true - # Enable cleanup listener for SecurityHub - cleanup: false - # Delay between AWS GetFindings API calls, to avoid hitting the API RequestLimit + # -- (optional) Used company name, defaults to "Kyverno" + companyName: "" + # -- Enable cleanup listener for SecurityHub + synchronize: true + # -- Delay between AWS GetFindings API calls, to avoid hitting the API RequestLimit delayInSeconds: 2 - # Added as additional properties to each securityHub event - customFields: {} - # filter results send by namespaces, policies and priorities - filter: {} - # add additional s3 channels with different configurations and filters - channels: [] - - gcs: - # GCS (Google Cloud Storage) Service Accout Credentials - credentials: "" - # receive the credentials from an existing secret instead + # -- Read configuration from an already existing Secret secretRef: "" - # Mounted secret path by Secrets Controller, secret should be in json format + # -- Mounted secret path by Secrets Controller, secret should be in json format mountedSecret: "" - # GCS Bucket - bucket: "" - # minimum priority "" < info < warning < critical < error - minimumPriority: "" - # list of sources which should send to GCS + # -- Minimum severity: "" < info < low < medium < high < critical + minimumSeverity: "" + # -- List of sources which should send sources: [] - # Skip already existing PolicyReportResults on startup + # -- Skip already existing PolicyReportResults on startup skipExistingOnStartup: true - # Added as additional properties to each gcs event + # -- Added as additional labels customFields: {} - # filter results send by namespaces, policies and priorities + # -- Filter Results which should send to this target + # Wildcars for namespaces and policies are supported, you can either define exclude or include values + # Filters are available for all targets except the UI filter: {} - # add additional s3 channels with different configurations and filters + # -- List of channels to route results to different configurations channels: [] -# required when policy-reporter runs in HA mode and you have targets configured -# if no targets are configured, leaderElection is disabled automatically + # Authentication via PodIdentity is also supported + gcs: + # -- (optional) GCS (Google Cloud Storage) Service Accout Credentials + credentials: "" + # -- (required) GCS Bucket + bucket: "" + # -- Read configuration from an already existing Secret + secretRef: "" + # -- Mounted secret path by Secrets Controller, secret should be in json format + mountedSecret: "" + # -- Minimum severity: "" < info < low < medium < high < critical + minimumSeverity: "" + # -- List of sources which should send + sources: [] + # -- Skip already existing PolicyReportResults on startup + skipExistingOnStartup: true + # -- Added as additional labels + customFields: {} + # -- Filter Results which should send to this target + # Wildcars for namespaces and policies are supported, you can either define exclude or include values + # Filters are available for all targets except the UI + filter: {} + # -- List of channels to route results to different configurations + channels: [] + +# LeaderElection configuration for HA mode # will be enabled when replicaCount > 1 leaderElection: - enabled: false releaseOnCancel: true leaseDuration: 15 renewDeadline: 10 retryPeriod: 2 -# use redis as external result cache instead of the in memory cache redis: + # -- Enables Redis as external result cache, uses in memory cache by default enabled: false + # -- Redis host address: "" + # -- Redis database database: 0 + # -- Redis key prefix prefix: "policy-reporter" + # -- (optional) Username username: "" + # -- (optional) Password password: "" +database: + # -- Use an external Database, supported: mysql, postgres, mariadb + type: "" + # -- Database + database: "" + # -- Username + username: "" + # -- Password + password: "" + # -- Host Address + host: "" + # -- Enables SSL + enableSSL: false + # -- Instead of configure the individual values you can also provide an DSN string + # example postgres: postgres://postgres:password@localhost:5432/postgres?sslmode=disable + # example mysql: root:password@tcp(localhost:3306)/test?tls=false + dsn: "" + # -- Read configuration from an existing Secret + # supported fields: username, password, host, dsn, database + secretRef: "" + # Read configuration from a mounted Secret, required the information in JSON format + # supported fields: username, password, host, dsn, database + mountedSecret: "" + # enabled if replicaCount > 1 podDisruptionBudget: # -- Configures the minimum available pods for policy-reporter disruptions. @@ -722,43 +763,823 @@ podDisruptionBudget: # Cannot be used if `minAvailable` is set. maxUnavailable: -# Node labels for pod assignment +# -- Node labels for pod assignment # ref: https://kubernetes.io/docs/user-guide/node-selection/ nodeSelector: {} -# Tolerations for pod assignment +# -- Tolerations for pod assignment # ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ tolerations: [] -# Anti-affinity to disallow deploying client and master nodes on the same worker node +# -- Anti-affinity to disallow deploying client and master nodes on the same worker node affinity: {} -# Topology Spread Constraints to better spread pods +# -- Topology Spread Constraints to better spread pods topologySpreadConstraints: [] -# livenessProbe for policy-reporter +# -- Deployment livenessProbe for policy-reporter livenessProbe: - httpGet: - path: /healthz - port: http - -# readinessProbe for policy-reporter -readinessProbe: httpGet: path: /ready port: http +# -- Deployment readinessProbe for policy-reporter +readinessProbe: + httpGet: + path: /healthz + port: http + extraVolumes: + # -- Deployment volumeMounts volumeMounts: [] + # -- Deployment values volumes: [] -# If set the volume for sqlite is freely configurable below "- name: sqlite". If no value is set an emptyDir is used. +# -- If set the volume for sqlite is freely configurable below "- name: sqlite". If no value is set an emptyDir is used. sqliteVolume: {} # emptyDir: # sizeLimit: 10Mi -# If set the volume for /tmp is freely configurable below "- name: tmp". If no value is set an emptyDir is used. +# -- Allow additional env variables to be added +envVars: [] + +# -- Allow custom configuration of the /tmp volume tmpVolume: {} - # emptyDir: - # sizeLimit: 10Mi + +ui: + # -- (bool) Enable Policy Reporter UI + enabled: false + image: + # -- (string) Image registry + registry: ghcr.io + # -- (string) Image repository + repository: kyverno/policy-reporter-ui + # -- (string) Image PullPolicy + pullPolicy: IfNotPresent + # -- (string) Image tag + tag: "2.0.0-rc.1" + + # -- Deployment replica count + replicaCount: 1 + + # -- Temporary Directory to persist session data for authentication + tempDir: "/tmp" + + logging: + # -- Enables external api request logging + api: false + # -- Enables server access logging + server: false + # -- Log encoding + # possible encodings are console and json + encoding: console + # -- Log level + # default info + logLevel: 0 + + server: + # -- Application port + port: 8080 + # -- Enabled CORS header + cors: true + # -- Overwrites Request Host with Proxy Host and adds `X-Forwarded-Host` and `X-Origin-Host` headers + overwriteHost: true + + openIDConnect: + # -- Enable openID Connect authentication + enabled: false + # -- OpenID Connect Discovery URL + discoveryUrl: "" + # -- OpenID Connect Callback URL + callbackUrl: "" + # -- OpenID Connect ClientID + clientId: "" + # -- OpenID Connect ClientSecret + clientSecret: "" + # -- OpenID Connect allowed Scopes + scopes: [] + # -- Provide OpenID Connect configuration via Secret + # supported keys: `discoveryUrl`, `clientId`, `clientSecret` + secretRef: "" + + oauth: + # -- Enable openID Connect authentication + enabled: false + # -- OAuth2 Provider + # supported: amazon, gitlab, github, apple, google, yandex, azuread + provider: "" + # -- OpenID Connect Callback URL + callbackUrl: "" + # -- OpenID Connect ClientID + clientId: "" + # -- OpenID Connect ClientSecret + clientSecret: "" + # -- OpenID Connect allowed Scopes + scopes: [] + # -- Provide OpenID Connect configuration via Secret + # supported keys: `provider`, `clientId`, `clientSecret` + secretRef: "" + + # -- optional banner text + banner: "" + + # -- DisplayMode dark/light/colorblind/colorblinddark + # uses the OS configured prefered color scheme as default + displayMode: "" + + # -- Additional customizable dashboards + customBoards: [] + # - name: Team A + # namespaces: + # # -- list of displayed namespaces + # list: [] + # # -- selector for displayed namespaces + # selector: + # team: team-a + # sources: + # # -- list of displayed sources + # list: [] + # clusterScope: + # # -- disply cluster scoped resources and results + # enabled: false + + # -- source specific configurations + sources: [] + # -- kyverno specific UI confiurations + # - name: kyverno + # -- show results per category, other option: severity + # chartType: result + # -- enabled action button to generate PolicyExceptions from the UI + # exceptions: false + # -- exclude results or (cluster)kinds per source + # excludes: + # results: + # - warn + # - error + + ## -- Default Cluster name + name: Default + + # -- Connected Policy Reporter APIs + clusters: [] + # - name: default + # host: http://policy-reporter:8080 + # secretRef: "" + # skipTLS: false + # certificate: "" + # plugins: + # - name: kyverno + # host: http://policy-reporter-kyverno-plugin:8080 + + # -- Image pull secrets for image verification policies, this will define the `--imagePullSecrets` argument + imagePullSecrets: [] + # regcred: + # registry: foo.example.com + # username: foobar + # password: secret + + serviceAccount: + # -- Create ServiceAccount + create: true + # -- Enable ServiceAccount automaount + automount: true + # -- Annotations for the ServiceAccount + annotations: {} + # -- The ServiceAccount name + name: "" + + # -- list of extra manifests + extraManifests: [] + + # -- Add sidecar containers to the UI deployment + # sidecarContainers: + # oauth-proxy: + # image: quay.io/oauth2-proxy/oauth2-proxy:v7.6.0 + # args: + # - --upstream=http://127.0.0.1:8080 + # - --http-address=0.0.0.0:8081 + # - ... + # ports: + # - containerPort: 8081 + # name: oauth-proxy + # protocol: TCP + # resources: {} + sidecarContainers: {} + + # -- Additional annotations to add to each pod + podAnnotations: {} + + # -- Additional labels to add to each pod + podLabels: {} + + # -- Deployment update strategy. + # Ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy + updateStrategy: {} + # rollingUpdate: + # maxSurge: 1 + # maxUnavailable: 40% + # type: RollingUpdate + + # -- The number of revisions to keep + revisionHistoryLimit: 10 + + # -- Security context for the pod + podSecurityContext: + runAsUser: 1234 + runAsGroup: 1234 + + # -- Allow additional env variables to be added + envVars: [] + + rbac: + # -- Create RBAC resources + enabled: true + + securityContext: + runAsUser: 1234 + runAsNonRoot: true + privileged: false + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + + service: + # -- Service type. + type: ClusterIP + # -- Service port. + port: 8080 + # -- Service annotations. + annotations: {} + # -- Service labels. + labels: {} + # -- Additional service ports for e.g. Sidecars # - name: authenticated + # additionalPorts: + # - name: authenticated + # port: 8081 + # targetPort: 8081 + additionalPorts: [] + + ingress: + # -- Create ingress resource. + enabled: false + # -- Redirect ingress to an additional defined port on the service + port: null + # -- Ingress class name. + className: "" + # -- Ingress labels. + labels: {} + # -- Ingress annotations. + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + # -- List of ingress host configurations. + hosts: [] + # - host: chart-example.local + # paths: + # - path: / + # pathType: ImplementationSpecific + # -- List of ingress TLS configurations. + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + + networkPolicy: + # -- When true, use a NetworkPolicy to allow ingress to the webhook + # This is useful on clusters using Calico and/or native k8s network policies in a default-deny setup. + enabled: false + # -- A list of valid from selectors according to https://kubernetes.io/docs/concepts/services-networking/network-policies. + # Enables Kubernetes API Server by default + egress: + - ports: + - protocol: TCP + port: 6443 + # -- A list of valid from selectors according to https://kubernetes.io/docs/concepts/services-networking/network-policies. + ingress: [] + + # -- Resource constraints + resources: {} + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + + # enabled if replicaCount > 1 + podDisruptionBudget: + # -- Configures the minimum available pods for kyvernoPlugin disruptions. + # Cannot be used if `maxUnavailable` is set. + minAvailable: 1 + # -- Configures the maximum unavailable pods for kyvernoPlugin disruptions. + # Cannot be used if `minAvailable` is set. + maxUnavailable: + + # -- Node labels for pod assignment + nodeSelector: {} + + # -- List of node taints to tolerate + tolerations: [] + + # -- Affinity constraints. + affinity: {} + +plugin: + kyverno: + # -- (bool) Enable Kyverno Plugin + enabled: false + image: + # -- (string) Image registry + registry: ghcr.io + # -- (string) Image repository + repository: kyverno/policy-reporter/kyverno-plugin + # -- (string) Image PullPolicy + pullPolicy: IfNotPresent + # -- (string) Image tag + # Defaults to `Chart.AppVersion` if omitted + tag: "0.3.0" + + # -- Deployment replica count + replicaCount: 1 + + logging: + # -- Enables external API request logging + api: false + # -- Enables Server access logging + server: false + # -- log encoding + # possible encodings are console and json + encoding: console + # -- log level + # default info + logLevel: 0 + + server: + # -- Application port + port: 8080 + + blockReports: + # -- Enables he BlockReport feature + enabled: false + # -- Watches for Kyverno Events in the configured namespace + # leave blank to watch in all namespaces + eventNamespace: default + results: + # -- Max items per PolicyReport resource + maxPerReport: 200 + # -- Keep only the latest of duplicated events + keepOnlyLatest: false + + # -- Image pull secrets for image verification policies, this will define the `--imagePullSecrets` argument + imagePullSecrets: [] + # regcred: + # registry: foo.example.com + # username: foobar + # password: secret + + serviceAccount: + # -- Create ServiceAccount + create: true + # -- Enable ServiceAccount automaount + automount: true + # -- Annotations for the ServiceAccount + annotations: {} + # -- The ServiceAccount name + name: "" + + # -- Additional annotations to add to each pod + podAnnotations: {} + + # -- Additional labels to add to each pod + podLabels: {} + + # -- Deployment update strategy. + # Ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy + updateStrategy: {} + # rollingUpdate: + # maxSurge: 1 + # maxUnavailable: 40% + # type: RollingUpdate + + # -- The number of revisions to keep + revisionHistoryLimit: 10 + + # -- Security context for the pod + podSecurityContext: + runAsUser: 1234 + runAsGroup: 1234 + + # -- Allow additional env variables to be added + envVars: [] + + rbac: + # -- Create RBAC resources + enabled: true + + securityContext: + runAsUser: 1234 + runAsNonRoot: true + privileged: false + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + + service: + # -- Service type. + type: ClusterIP + # -- Service port. + port: 8080 + # -- Service annotations. + annotations: {} + # -- Service labels. + labels: {} + + ingress: + # -- Create ingress resource. + enabled: false + # -- Ingress class name. + className: "" + # -- Ingress labels. + labels: {} + # -- Ingress annotations. + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + # -- List of ingress host configurations. + hosts: [] + # - host: chart-example.local + # paths: + # - path: / + # pathType: ImplementationSpecific + # -- List of ingress TLS configurations. + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + + networkPolicy: + # -- When true, use a NetworkPolicy to allow ingress to the webhook + # This is useful on clusters using Calico and/or native k8s network policies in a default-deny setup. + enabled: false + # -- A list of valid from selectors according to https://kubernetes.io/docs/concepts/services-networking/network-policies. + # Enables Kubernetes API Server by default + egress: + - ports: + - protocol: TCP + port: 6443 + # -- A list of valid from selectors according to https://kubernetes.io/docs/concepts/services-networking/network-policies. + ingress: [] + + # -- Resource constraints + resources: {} + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + + # required for HA mode + # if "blockReports" is disabled, leaderElection is also disabled automatically + # will be enabled when replicaCount > 1 + leaderElection: + # -- Lock Name + lockName: kyverno-plugin + # -- Released lock when the run context is cancelled. + releaseOnCancel: true + # -- LeaseDuration is the duration that non-leader candidates will wait to force acquire leadership. + leaseDuration: 15 + # -- RenewDeadline is the duration that the acting master will retry refreshing leadership before giving up. + renewDeadline: 10 + # -- RetryPeriod is the duration the LeaderElector clients should wait between tries of actions. + retryPeriod: 2 + + # enabled if replicaCount > 1 + podDisruptionBudget: + # -- Configures the minimum available pods for kyvernoPlugin disruptions. + # Cannot be used if `maxUnavailable` is set. + minAvailable: 1 + # -- Configures the maximum unavailable pods for kyvernoPlugin disruptions. + # Cannot be used if `minAvailable` is set. + maxUnavailable: + + # -- Node labels for pod assignment + nodeSelector: {} + + # -- List of node taints to tolerate + tolerations: [] + + # -- Affinity constraints. + affinity: {} + + trivy: + # -- (bool) Enable Trivy Operator Plugin + enabled: false + image: + # -- (string) Image registry + registry: ghcr.io + # -- (string) Image repository + repository: kyverno/policy-reporter/trivy-plugin + # -- (string) Image PullPolicy + pullPolicy: IfNotPresent + # -- (string) Image tag + # Defaults to `Chart.AppVersion` if omitted + tag: "0.2.0" + + # -- Deployment replica count + replicaCount: 1 + + logging: + # -- Enables external API request logging + api: false + # -- Enables Server access logging + server: false + # -- log encoding + # possible encodings are console and json + encoding: console + # -- log level + # default info + logLevel: 0 + + server: + # -- Application port + port: 8080 + + policyReporter: + # -- Skip TLS Verification + skipTLS: false + # -- TLS Certificate + certificate: "" + # -- Secret to read the API configuration from + # supports `host`, `certificate`, `skipTLS`, `username`, `password` key + secretRef: "" + + # -- Image pull secrets for image verification policies, this will define the `--imagePullSecrets` argument + imagePullSecrets: [] + # regcred: + # registry: foo.example.com + # username: foobar + # password: secret + + serviceAccount: + # -- Create ServiceAccount + create: true + # -- Enable ServiceAccount automaount + automount: true + # -- Annotations for the ServiceAccount + annotations: {} + # -- The ServiceAccount name + name: "" + + # -- Additional annotations to add to each pod + podAnnotations: {} + + # -- Additional labels to add to each pod + podLabels: {} + + # -- Deployment update strategy. + # Ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy + updateStrategy: {} + # rollingUpdate: + # maxSurge: 1 + # maxUnavailable: 40% + # type: RollingUpdate + + # -- The number of revisions to keep + revisionHistoryLimit: 10 + + # -- Security context for the pod + podSecurityContext: + runAsUser: 1234 + runAsGroup: 1234 + + # -- Allow additional env variables to be added + envVars: [] + + rbac: + # -- Create RBAC resources + enabled: true + + securityContext: + runAsUser: 1234 + runAsNonRoot: true + privileged: false + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + + service: + # -- Service type. + type: ClusterIP + # -- Service port. + port: 8080 + # -- Service annotations. + annotations: {} + # -- Service labels. + labels: {} + + ingress: + # -- Create ingress resource. + enabled: false + # -- Ingress class name. + className: "" + # -- Ingress labels. + labels: {} + # -- Ingress annotations. + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + # -- List of ingress host configurations. + hosts: [] + # - host: chart-example.local + # paths: + # - path: / + # pathType: ImplementationSpecific + # -- List of ingress TLS configurations. + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + + networkPolicy: + # -- When true, use a NetworkPolicy to allow ingress to the webhook + # This is useful on clusters using Calico and/or native k8s network policies in a default-deny setup. + enabled: false + # -- A list of valid from selectors according to https://kubernetes.io/docs/concepts/services-networking/network-policies. + # Enables Kubernetes API Server by default + egress: + - ports: + - protocol: TCP + port: 6443 + # -- A list of valid from selectors according to https://kubernetes.io/docs/concepts/services-networking/network-policies. + ingress: [] + + # -- Resource constraints + resources: {} + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + + # enabled if replicaCount > 1 + podDisruptionBudget: + # -- Configures the minimum available pods for kyvernoPlugin disruptions. + # Cannot be used if `maxUnavailable` is set. + minAvailable: 1 + # -- Configures the maximum unavailable pods for kyvernoPlugin disruptions. + # Cannot be used if `minAvailable` is set. + maxUnavailable: + + # -- Node labels for pod assignment + nodeSelector: {} + + # -- List of node taints to tolerate + tolerations: [] + + # -- Affinity constraints. + affinity: {} + +monitoring: + # -- Enables the Prometheus Operator integration + enabled: false + + # -- Key/value pairs that are attached to all resources. + annotations: {} + + serviceMonitor: + # -- HonorLabels chooses the metrics labels on collisions with target labels + honorLabels: false + # -- Allow to override the namespace for serviceMonitor + namespace: + # -- Labels to match the serviceMonitorSelector of the Prometheus Resource + labels: {} + # -- ServiceMonitor Relabelings + # https://github.com/prometheus-operator/prometheus-operator/blob/main/Documentation/api.md#relabelconfig + relabelings: [] + # -- See serviceMonitor.relabelings + metricRelabelings: [] + # -- (optional) NamespaceSelector + namespaceSelector: {} + # -- (optional) ScrapeTimeout + scrapeTimeout: + # -- (optional) Scrape interval + interval: + + grafana: + # -- Naamespace for configMap of grafana dashboards + namespace: + dashboards: + # -- Enable the deployment of grafana dashboards + enabled: true + # -- Label to find dashboards using the k8s sidecar + label: grafana_dashboard + # -- Label value to find dashboards using the k8s sidecar + value: "1" + # -- List of custom label filter + # Used to add filter for report label based metric labels defined in custom mode + labelFilter: [] + multicluster: + # -- Enable cluster filter in all dashboards + enabled: false + # -- Metric Label which is used to filter clusters + label: cluster + enable: + # -- Enable the Overview Dashboard + overview: true + # -- Enable the PolicyReport Dashboard + policyReportDetails: true + # -- Enable the ClusterPolicyReport Dashboard + clusterPolicyReportDetails: true + folder: + # -- Annotation to enable folder storage using the k8s sidecar + annotation: grafana_folder + # -- Grafana folder in which to store the dashboards + name: Policy Reporter + datasource: + # -- Grafana Datasource Label + label: Prometheus + # -- Grafana Datasource PluginId + pluginId: prometheus + # -- Grafana Datasource PluginName + pluginName: Prometheus + + grafanaDashboard: + # -- Create GrafanaDashboard custom resource referencing to the configMap. + # according to https://grafana-operator.github.io/grafana-operator/docs/examples/dashboard_from_configmap/readme/ + enabled: false + # -- Dashboard folder + folder: kyverno + # -- Allow cross Namespace import + allowCrossNamespaceImport: true + # -- Label match selector + matchLabels: + dashboards: "grafana" + + # Customize the Grafana PolicyReport Dashboard + policyReportDetails: + firstStatusRow: + height: 8 + secondStatusRow: + enabled: true + height: 2 + statusTimeline: + enabled: true + height: 8 + passTable: + enabled: true + height: 8 + failTable: + enabled: true + height: 8 + warningTable: + enabled: true + height: 4 + errorTable: + enabled: true + height: 4 + + # Customize the Grafana ClusterPolicyReport Dashboard + clusterPolicyReportDetails: + statusRow: + height: 6 + statusTimeline: + enabled: true + height: 8 + passTable: + enabled: true + height: 8 + failTable: + enabled: true + height: 8 + warningTable: + enabled: true + height: 4 + errorTable: + enabled: true + height: 4 + + # Customize the Grafana Overview Dashboard + policyReportOverview: + failingSummaryRow: + height: 8 + failingTimeline: + height: 10 + failingPolicyRuleTable: + height: 10 + failingClusterPolicyRuleTable: + height: 10 diff --git a/cmd/run.go b/cmd/run.go index 43e00f05..c2bf64a5 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -11,6 +11,9 @@ import ( "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" + "github.com/kyverno/policy-reporter/pkg/api" + v1 "github.com/kyverno/policy-reporter/pkg/api/v1" + v2 "github.com/kyverno/policy-reporter/pkg/api/v2" "github.com/kyverno/policy-reporter/pkg/config" "github.com/kyverno/policy-reporter/pkg/database" "github.com/kyverno/policy-reporter/pkg/listener" @@ -41,6 +44,8 @@ func newRunCMD(version string) *cobra.Command { k8sConfig.Burst = c.K8sClient.Burst readinessProbe := config.NewReadinessProbe(c) + defer readinessProbe.Close() + resolver := config.NewResolver(c, k8sConfig) logger, err := resolver.Logger() if err != nil { @@ -52,11 +57,25 @@ func newRunCMD(version string) *cobra.Command { return err } - server := resolver.APIServer(cmd.Context(), client.HasSynced) + secretInformer, err := resolver.SecretInformer() + if err != nil { + return err + } g := &errgroup.Group{} var store *database.Store + servOptions := []api.ServerOption{ + api.WithPort(c.API.Port), + api.WithHealthChecks([]api.HealthCheck{ + func() error { + if !client.HasSynced() { + return errors.New("informer not ready") + } + return nil + }, + }), + } if c.REST.Enabled { db := resolver.Database() @@ -65,7 +84,12 @@ func newRunCMD(version string) *cobra.Command { } defer db.Close() - store, err = resolver.PolicyReportStore(db) + store, err = resolver.Store(db) + if err != nil { + return err + } + + nsClient, err := resolver.NamespaceClient() if err != nil { return err } @@ -76,18 +100,18 @@ func newRunCMD(version string) *cobra.Command { } logger.Info("REST api enabled") - server.RegisterV1Handler(store, resolver.ViolationsReporter()) + servOptions = append(servOptions, v1.WithAPI(store, resolver.TargetClients(), resolver.ViolationsReporter()), v2.WithAPI(store, nsClient, c.Targets)) } if c.Metrics.Enabled { logger.Info("metrics enabled") resolver.RegisterMetricsListener() - server.RegisterMetricsHandler() + servOptions = append(servOptions, api.WithMetrics()) } if c.Profiling.Enabled { logger.Info("pprof profiling enabled") - server.RegisterProfilingHandler() + servOptions = append(servOptions, api.WithProfiling()) } if !resolver.ResultCache().Shared() { @@ -145,9 +169,15 @@ func newRunCMD(version string) *cobra.Command { readinessProbe.Ready() } + server, err := resolver.Server(cmd.Context(), servOptions) + if err != nil { + return err + } + g.Go(server.Start) g.Go(func() error { + logger.Info("wait policy informer") readinessProbe.Wait() logger.Info("start client", zap.Int("worker", c.WorkerCount)) @@ -162,6 +192,26 @@ func newRunCMD(version string) *cobra.Command { } }) + g.Go(func() error { + collection := resolver.TargetClients() + if !collection.UsesSecrets() { + return nil + } + + readinessProbe.Wait() + + stop := make(chan struct{}) + if err := secretInformer.Sync(collection, stop); err != nil { + zap.L().Error("secret informer error", zap.Error(err)) + + return err + } + + <-stop + + return nil + }) + return g.Wait() }, } @@ -169,8 +219,8 @@ func newRunCMD(version string) *cobra.Command { // For local usage cmd.PersistentFlags().StringP("kubeconfig", "k", "", "absolute path to the kubeconfig file") cmd.PersistentFlags().StringP("config", "c", "", "target configuration file") - cmd.PersistentFlags().IntP("port", "p", 8080, "http port for the optional rest api") - cmd.PersistentFlags().StringP("dbfile", "d", "sqlite-database.db", "path to the SQLite DB File") + cmd.PersistentFlags().IntP("port", "p", 8001, "http port for the optional rest api") + cmd.PersistentFlags().StringP("dbfile", "d", "sqlite-database-v2.db", "path to the SQLite DB File") cmd.PersistentFlags().BoolP("metrics-enabled", "m", false, "Enable Policy Reporter's Metrics API") cmd.PersistentFlags().BoolP("rest-enabled", "r", false, "Enable Policy Reporter's REST API") cmd.PersistentFlags().Bool("profile", false, "Enable application profiling with pprof") diff --git a/cmd/send/summary.go b/cmd/send/summary.go index 34b2da3f..beeb7bfb 100644 --- a/cmd/send/summary.go +++ b/cmd/send/summary.go @@ -78,6 +78,12 @@ func NewSummaryCMD() *cobra.Command { logger.Sugar().Infof("email sent to %s\n", strings.Join(c.EmailReports.Summary.To, ", ")) }() + nsclient, err := resolver.NamespaceClient() + if err != nil { + logger.Error("failed to get namespace client", zap.Error(err)) + return err + } + for _, ch := range c.EmailReports.Violations.Channels { go func(channel config.EmailReport) { defer wg.Done() @@ -87,7 +93,7 @@ func NewSummaryCMD() *cobra.Command { return } - sources := summary.FilterSources(data, config.EmailReportFilterFromConfig(channel.Filter), !channel.Filter.DisableClusterReports) + sources := summary.FilterSources(data, config.EmailReportFilterFromConfig(nsclient, channel.Filter), !channel.Filter.DisableClusterReports) if len(sources) == 0 { logger.Info("skip email - no results to send") return diff --git a/cmd/send/violations.go b/cmd/send/violations.go index a30cf1c0..edf0c92e 100644 --- a/cmd/send/violations.go +++ b/cmd/send/violations.go @@ -78,6 +78,12 @@ func NewViolationsCMD() *cobra.Command { logger.Sugar().Infof("email sent to %s\n", strings.Join(c.EmailReports.Violations.To, ", ")) }() + nsclient, err := resolver.NamespaceClient() + if err != nil { + logger.Error("failed to get namespace client", zap.Error(err)) + return err + } + for _, ch := range c.EmailReports.Violations.Channels { go func(channel config.EmailReport) { defer wg.Done() @@ -87,7 +93,7 @@ func NewViolationsCMD() *cobra.Command { return } - sources := violations.FilterSources(data, config.EmailReportFilterFromConfig(channel.Filter), !channel.Filter.DisableClusterReports) + sources := violations.FilterSources(data, config.EmailReportFilterFromConfig(nsclient, channel.Filter), !channel.Filter.DisableClusterReports) if len(sources) == 0 { logger.Info("skip email - no results to send") return diff --git a/docs/CUSTOM_BOARDS.md b/docs/CUSTOM_BOARDS.md new file mode 100644 index 00000000..fc5229df --- /dev/null +++ b/docs/CUSTOM_BOARDS.md @@ -0,0 +1,70 @@ +# Policy Reporter UI - Custom Boards + +CustomBoards allows you to configure additional dashboards with a custom subset of sources and namespaces, selected via a list and/or label selector + +## Example CustomBoard Config + +![Custom Boards](https://github.com/kyverno/policy-reporter/blob/3.x/docs/images/custom-boards/list.png) + +```yaml +ui: + enabled: true + + customBoards: + - name: System + namespaces: + list: + - kube-system + - kyverno + - policy-reporter +``` + +### CustomBoard with NamespaceSelector + +![Custom Boards](https://github.com/kyverno/policy-reporter/blob/3.x/docs/images/custom-boards/selector.png) + +```yaml +ui: + enabled: true + + customBoards: + - name: System + namespaces: + selector: + group: system +``` + +### CustomBoard with ClusterResources +![Custom Boards](https://github.com/kyverno/policy-reporter/blob/3.x/docs/images/custom-boards/cluster.png) + +```yaml +ui: + enabled: true + + customBoards: + - name: System + clusterScope: + enabled: true + namespaces: + selector: + group: system +``` + +### CustomBoard with Source List + +![Custom Boards](https://github.com/kyverno/policy-reporter/blob/3.x/docs/images/custom-boards/source.png) + +```yaml +ui: + enabled: true + + customBoards: + - name: System + clusterScope: + enabled: true + namespaces: + selector: + group: system + sources: + list: [kyverno] +``` diff --git a/docs/EXCEPTIONS.md b/docs/EXCEPTIONS.md new file mode 100644 index 00000000..923b83a2 --- /dev/null +++ b/docs/EXCEPTIONS.md @@ -0,0 +1,64 @@ +# Policy Reporter UI - Generate Kyverno PolicyExceptions + +The Policy Reporter UI provides a visual overview of the policy status in your cluster, but no action you can take to change the status by default. + +In the case of Kyverno, you have two options for dealing with policy failure. You can either fix it or create an exception for it. While the first option is difficult to automate and not always possible, creating an exception is relatively easy and can help exclude resources from validation that you are not able to fix immediately. + +To support this process, the new Policy Reporter plugin system provides an Exception API that can be used to implement source-specific logic for PolicyException creation. The new Policy Reporter Kyverno plugin utilizes this API to provide an automated method for generating Kyverno PolicyException CRD resources that excludes a single or all failed policies depending on the context in the UI. + +## Configuration + +Because the Exception API is part of the Policy Reporter Kyverno Plugin, its required to install this plugin to use it and enable the exception feature. + +### Helm 3 Configuration + +```yaml +plugin: + kyverno: + enabled: true + +ui: + enabled: true + sources: + - name: kyverno + exceptions: true + excludes: + namespaceKinds: + - Pod + - Job + - ReplicaSet + results: + - warn + - error +``` + +### Alternative manual UI Configuration + +```yaml +# Configure the Kyverno Plugin the Cluster config +clusters: +- name: Default + host: http://policy-reporter:8080 + plugins: + - name: kyverno + host: http://policy-reporter-kyverno-plugin:8080/api + +# Enable `exceptions` in the kyverno source configuration +sources: + - name: kyverno + exceptions: true + excludes: + namespaceKinds: + - Pod + - Job + - ReplicaSet + results: + - warn + - error +``` + +### Examples + +![Exception Resource List](https://github.com/kyverno/policy-reporter/blob/3.x/docs/images/exceptions/resource-list.png) + +![Exception Dialog](https://github.com/kyverno/policy-reporter/blob/3.x/docs/images/exceptions/exception-dialog.png) diff --git a/docs/SETUP.md b/docs/SETUP.md new file mode 100644 index 00000000..e49af359 --- /dev/null +++ b/docs/SETUP.md @@ -0,0 +1,30 @@ +# Setup Kyverno and Policy Reporter + +## Install Kyverno + Kyverno PSS Policies + +Add Helm Repo + +```bash +helm repo add kyverno https://kyverno.github.io/kyverno/ +helm repo update +``` + +Install Kyverno + PSS Policies + +```bash +helm upgrade --install kyverno kyverno/kyverno -n kyverno --create-namespace +helm upgrade --install kyverno-policies kyverno/kyverno-policies -n kyverno --create-namespace --set podSecurityStandard=restricted +``` + +## Installing Policy Reporter v3 Preview and Policy Reporter UI v2 + Kyverno Plugin + +```bash +helm repo add policy-reporter https://kyverno.github.io/policy-reporter +helm repo update +``` + +Install the Policy Reporter Preview + +```bash +helm upgrade --install policy-reporter policy-reporter/policy-reporter --create-namespace -n policy-reporter --devel --set ui.enabled=true --set plugin.kyverno.enabled=true +``` \ No newline at end of file diff --git a/docs/UI_AUTH.md b/docs/UI_AUTH.md new file mode 100644 index 00000000..c7d25b18 --- /dev/null +++ b/docs/UI_AUTH.md @@ -0,0 +1,104 @@ +# Configure Authentication for Policy Reporter UI + +With Policy Reporter UI v2 it is possible to use either OAuth2 or OpenIDConnect as authentication mechanism. + +Its not possible to reduce or configure view permission based on roles or any other information yet. +Authentication ensures that no unauthorized person is able to open the UI at all. + +## OAuth2 + +Policy Reporter UI v2 supports a fixed set of oauth2 providers. If the provider of your choice is not yet supported, you can submit a feature request for it. + +### Supported OAuth Provider + +* amazon +* gitlab +* github +* apple +* google +* yandex +* azuread + +### Example Configuration (GitHub Provider) + +Since the callback URL depends on your setup, you must explicitly configure it. + +```yaml +ui: + oauth: + enabled: true + clientId: c79c02881aa1... + clientSecret: fb2035255d0bd182c9... + provider: github + callback: http://localhost:8082/callback + scopes: [] +``` + +### Example SecretRef + +Instead of providing the information directly in the values, you can also fetch the information from an existing secret. + +#### Values + +```yaml +ui: + oauth: + enabled: true + callback: http://localhost:8082/callback + scopes: [] + secretRef: 'github-provider' +``` +#### Secret + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: github-provider +data: + clientId: Yzc5YzAyODgxYWEx + clientSecret: ZmIyMDM1MjU1ZDBiZDE4MmM5 + provider: Z2l0aHVi +``` + +## OpenIDConnect + +This authentication mechanism supports all compatible services and systems. + +### Example Configuration (Keycloak) + +```yaml +ui: + openIDConnect: + enabled: true + clientId: policy-reporter + clientSecret: c11cYF9tNtL94w.... + callbackUrl: http://localhost:8082/callback + discoveryUrl: 'https://keycloak.instance.de/realms/timetracker' +``` + +### Example SecretRef + +Instead of providing the information directly in the values, you can also fetch the information from an existing secret. + +#### Values + +```yaml +ui: + openIDConnect: + enabled: true + callback: http://localhost:8082/callback + secretRef: 'keycloak-provider' +``` +#### Secret + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: keycloak-provider +data: + clientId: Yzc5YzAyODgxYWEx + clientSecret: ZmIyMDM1MjU1ZDBiZDE4MmM5 + discoveryUrl: aHR0cHM6Ly9rZXljbG9hay5pbnN0YW5jZS5kZS9yZWFsbXMvdGltZXRyYWNrZXI= +``` diff --git a/docs/images/custom-boards/cluster.png b/docs/images/custom-boards/cluster.png new file mode 100644 index 00000000..ad6fcda5 Binary files /dev/null and b/docs/images/custom-boards/cluster.png differ diff --git a/docs/images/custom-boards/list.png b/docs/images/custom-boards/list.png new file mode 100644 index 00000000..8e7403f2 Binary files /dev/null and b/docs/images/custom-boards/list.png differ diff --git a/docs/images/custom-boards/selector.png b/docs/images/custom-boards/selector.png new file mode 100644 index 00000000..d26f56e9 Binary files /dev/null and b/docs/images/custom-boards/selector.png differ diff --git a/docs/images/custom-boards/source.png b/docs/images/custom-boards/source.png new file mode 100644 index 00000000..d284975f Binary files /dev/null and b/docs/images/custom-boards/source.png differ diff --git a/docs/images/exceptions/exception-dialog.png b/docs/images/exceptions/exception-dialog.png new file mode 100644 index 00000000..982c6aa2 Binary files /dev/null and b/docs/images/exceptions/exception-dialog.png differ diff --git a/docs/images/exceptions/resource-list.png b/docs/images/exceptions/resource-list.png new file mode 100644 index 00000000..2c722232 Binary files /dev/null and b/docs/images/exceptions/resource-list.png differ diff --git a/docs/images/screen.png b/docs/images/screen.png new file mode 100644 index 00000000..38ef9dd9 Binary files /dev/null and b/docs/images/screen.png differ diff --git a/go.mod b/go.mod index f257eef5..8e1f1183 100644 --- a/go.mod +++ b/go.mod @@ -1,145 +1,191 @@ module github.com/kyverno/policy-reporter -go 1.22.1 +go 1.23.0 require ( - cloud.google.com/go/storage v1.40.0 - github.com/aws/aws-sdk-go-v2 v1.27.2 - github.com/aws/aws-sdk-go-v2/config v1.27.18 - github.com/aws/aws-sdk-go-v2/credentials v1.17.18 - github.com/aws/aws-sdk-go-v2/service/kinesis v1.27.10 - github.com/aws/aws-sdk-go-v2/service/s3 v1.55.1 - github.com/aws/aws-sdk-go-v2/service/securityhub v1.49.2 - github.com/aws/aws-sdk-go-v2/service/sts v1.28.12 + cloud.google.com/go/storage v1.44.0 + github.com/atc0005/go-teams-notify/v2 v2.13.0 + github.com/aws/aws-sdk-go-v2 v1.32.0 + github.com/aws/aws-sdk-go-v2/config v1.27.41 + github.com/aws/aws-sdk-go-v2/credentials v1.17.39 + github.com/aws/aws-sdk-go-v2/service/kinesis v1.32.0 + github.com/aws/aws-sdk-go-v2/service/s3 v1.65.0 + github.com/aws/aws-sdk-go-v2/service/securityhub v1.54.0 + github.com/aws/aws-sdk-go-v2/service/sts v1.32.0 + github.com/gin-contrib/gzip v1.0.1 + github.com/gin-contrib/pprof v1.5.0 + github.com/gin-contrib/zap v1.1.4 + github.com/gin-gonic/gin v1.10.0 github.com/go-redis/redis/v8 v8.11.5 github.com/go-sql-driver/mysql v1.8.1 + github.com/google/uuid v1.6.0 github.com/kyverno/go-wildcard v1.0.5 - github.com/mattn/go-sqlite3 v1.14.22 + github.com/mattn/go-sqlite3 v1.14.24 github.com/patrickmn/go-cache v2.1.0+incompatible - github.com/prometheus/client_golang v1.19.0 + github.com/prometheus/client_golang v1.20.4 github.com/prometheus/client_model v0.6.1 github.com/segmentio/fasthash v1.0.3 - github.com/spf13/cobra v1.8.0 - github.com/spf13/viper v1.18.2 - github.com/uptrace/bun v1.2.1 - github.com/uptrace/bun/dialect/mysqldialect v1.2.1 - github.com/uptrace/bun/dialect/pgdialect v1.2.1 - github.com/uptrace/bun/dialect/sqlitedialect v1.2.1 - github.com/uptrace/bun/driver/pgdriver v1.2.1 + github.com/slack-go/slack v0.14.0 + github.com/spf13/cobra v1.8.1 + github.com/spf13/viper v1.19.0 + github.com/stretchr/testify v1.9.0 + github.com/uptrace/bun v1.2.3 + github.com/uptrace/bun/dialect/mysqldialect v1.2.3 + github.com/uptrace/bun/dialect/pgdialect v1.2.3 + github.com/uptrace/bun/dialect/sqlitedialect v1.2.3 + github.com/uptrace/bun/driver/pgdriver v1.2.3 + github.com/uptrace/bun/extra/bundebug v1.2.3 github.com/xhit/go-simple-mail/v2 v2.16.0 go.uber.org/zap v1.27.0 - golang.org/x/sync v0.7.0 - google.golang.org/api v0.174.0 - k8s.io/apimachinery v0.30.0 - k8s.io/client-go v0.30.0 + golang.org/x/sync v0.8.0 + golang.org/x/text v0.19.0 + google.golang.org/api v0.199.0 + k8s.io/apimachinery v0.31.1 + k8s.io/client-go v0.31.1 ) require ( - cloud.google.com/go v0.112.2 // indirect - cloud.google.com/go/auth v0.2.0 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.0 // indirect - cloud.google.com/go/compute/metadata v0.3.0 // indirect - cloud.google.com/go/iam v1.1.7 // indirect + cel.dev/expr v0.16.2 // indirect + cloud.google.com/go v0.115.1 // indirect + cloud.google.com/go/auth v0.9.7 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect + cloud.google.com/go/compute/metadata v0.5.2 // indirect + cloud.google.com/go/iam v1.2.1 // indirect + cloud.google.com/go/monitoring v1.21.1 // indirect filippo.io/edwards25519 v1.1.0 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.5 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.9 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.9 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.9 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.11 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.11 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.9 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.20.11 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.5 // indirect - github.com/aws/smithy-go v1.20.2 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.2 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.2 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.2 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.15 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.19 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.19 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.19 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.24.0 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.0 // indirect + github.com/aws/smithy-go v1.22.0 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/bytedance/sonic v1.12.3 // indirect + github.com/bytedance/sonic/loader v0.2.0 // indirect + github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/emicklei/go-restful/v3 v3.12.0 // indirect - github.com/evanphx/json-patch v5.9.0+incompatible // indirect + github.com/emicklei/go-restful/v3 v3.12.1 // indirect + github.com/envoyproxy/go-control-plane v0.13.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect + github.com/fatih/color v1.17.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.5 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.22.1 // indirect github.com/go-test/deep v1.0.8 // indirect + github.com/goccy/go-json v0.10.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/google/s2a-go v0.1.7 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect - github.com/googleapis/gax-go/v2 v2.12.3 // indirect + github.com/google/s2a-go v0.1.8 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect + github.com/googleapis/gax-go/v2 v2.13.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.10 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pelletier/go-toml/v2 v2.2.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus/procfs v0.13.0 // indirect - github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect + github.com/sagikazarmark/locafero v0.6.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/cast v1.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect github.com/toorop/go-dkim v0.0.0-20240103092955-90b7d1423f92 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/x448/float16 v0.8.4 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.50.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0 // indirect - go.opentelemetry.io/otel v1.25.0 // indirect - go.opentelemetry.io/otel/metric v1.25.0 // indirect - go.opentelemetry.io/otel/trace v1.25.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.30.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect + go.opentelemetry.io/otel v1.30.0 // indirect + go.opentelemetry.io/otel/metric v1.30.0 // indirect + go.opentelemetry.io/otel/sdk v1.30.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.30.0 // indirect + go.opentelemetry.io/otel/trace v1.30.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.22.0 // indirect - golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/text v0.14.0 // indirect - google.golang.org/genproto v0.0.0-20240415180920-8c6c420018be // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // indirect - google.golang.org/grpc v1.63.2 // indirect - google.golang.org/protobuf v1.33.0 // indirect + golang.org/x/arch v0.11.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6 // indirect + golang.org/x/mod v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect + google.golang.org/genproto v0.0.0-20240930140551-af27646dc61f // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240930140551-af27646dc61f // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f // indirect + google.golang.org/grpc v1.67.1 // indirect + google.golang.org/grpc/stats/opentelemetry v0.0.0-20241004113128-859602c14c6c // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/klog/v2 v2.120.1 // indirect - k8s.io/utils v0.0.0-20240310230437-4693a0247e57 // indirect - mellium.im/sasl v0.3.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 // indirect + mellium.im/sasl v0.3.2 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) require ( - github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/imdario/mergo v0.3.16 // indirect - github.com/prometheus/common v0.52.3 // indirect + github.com/prometheus/common v0.60.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect - golang.org/x/net v0.24.0 // indirect - golang.org/x/oauth2 v0.19.0 - golang.org/x/term v0.19.0 // indirect - golang.org/x/time v0.5.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/oauth2 v0.23.0 + golang.org/x/term v0.25.0 // indirect + golang.org/x/time v0.7.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - k8s.io/api v0.30.0 - k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3 // indirect + k8s.io/api v0.31.1 + k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect ) diff --git a/go.sum b/go.sum index 849ded69..9c8456fc 100644 --- a/go.sum +++ b/go.sum @@ -1,90 +1,141 @@ +cel.dev/expr v0.16.2 h1:RwRhoH17VhAu9U5CMvMhH1PDVgf0tuz9FT+24AfMLfU= +cel.dev/expr v0.16.2/go.mod h1:gXngZQMkWJoSbE8mOzehJlXQyubn/Vg0vR9/F3W7iw8= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.112.2 h1:ZaGT6LiG7dBzi6zNOvVZwacaXlmf3lRqnC4DQzqyRQw= -cloud.google.com/go v0.112.2/go.mod h1:iEqjp//KquGIJV/m+Pk3xecgKNhV+ry+vVTsy4TbDms= -cloud.google.com/go/auth v0.2.0 h1:y6oTcpMSbOcXbwYgUUrvI+mrQ2xbrcdpPgtVbCGTLTk= -cloud.google.com/go/auth v0.2.0/go.mod h1:+yb+oy3/P0geX6DLKlqiGHARGR6EX2GRtYCzWOCQSbU= -cloud.google.com/go/auth/oauth2adapt v0.2.0 h1:FR8zevgQwu+8CqiOT5r6xCmJa3pJC/wdXEEPF1OkNhA= -cloud.google.com/go/auth/oauth2adapt v0.2.0/go.mod h1:AfqujpDAlTfLfeCIl/HJZZlIxD8+nJoZ5e0x1IxGq5k= -cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= -cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -cloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM= -cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA= -cloud.google.com/go/storage v1.40.0 h1:VEpDQV5CJxFmJ6ueWNsKxcr1QAYOXEgxDa+sBbJahPw= -cloud.google.com/go/storage v1.40.0/go.mod h1:Rrj7/hKlG87BLqDJYtwR0fbPld8uJPbQ2ucUMY7Ir0g= +cloud.google.com/go v0.115.1 h1:Jo0SM9cQnSkYfp44+v+NQXHpcHqlnRJk2qxh6yvxxxQ= +cloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc= +cloud.google.com/go/auth v0.9.7 h1:ha65jNwOfI48YmUzNfMaUDfqt5ykuYIUnSartpU1+BA= +cloud.google.com/go/auth v0.9.7/go.mod h1:Xo0n7n66eHyOWWCnitop6870Ilwo3PiZyodVkkH1xWM= +cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY= +cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc= +cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= +cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= +cloud.google.com/go/iam v1.2.1 h1:QFct02HRb7H12J/3utj0qf5tobFh9V4vR6h9eX5EBRU= +cloud.google.com/go/iam v1.2.1/go.mod h1:3VUIJDPpwT6p/amXRC5GY8fCCh70lxPygguVtI0Z4/g= +cloud.google.com/go/logging v1.11.0 h1:v3ktVzXMV7CwHq1MBF65wcqLMA7i+z3YxbUsoK7mOKs= +cloud.google.com/go/logging v1.11.0/go.mod h1:5LDiJC/RxTt+fHc1LAt20R9TKiUTReDg6RuuFOZ67+A= +cloud.google.com/go/longrunning v0.6.1 h1:lOLTFxYpr8hcRtcwWir5ITh1PAKUD/sG2lKrTSYjyMc= +cloud.google.com/go/longrunning v0.6.1/go.mod h1:nHISoOZpBcmlwbJmiVk5oDRz0qG/ZxPynEGs1iZ79s0= +cloud.google.com/go/monitoring v1.21.1 h1:zWtbIoBMnU5LP9A/fz8LmWMGHpk4skdfeiaa66QdFGc= +cloud.google.com/go/monitoring v1.21.1/go.mod h1:Rj++LKrlht9uBi8+Eb530dIrzG/cU/lB8mt+lbeFK1c= +cloud.google.com/go/storage v1.44.0 h1:abBzXf4UJKMmQ04xxJf9dYM/fNl24KHoTuBjyJDX2AI= +cloud.google.com/go/storage v1.44.0/go.mod h1:wpPblkIuMP5jCB/E48Pz9zIo2S/zD8g+ITmxKkPCITE= +cloud.google.com/go/trace v1.11.1 h1:UNqdP+HYYtnm6lb91aNA5JQ0X14GnxkABGlfz2PzPew= +cloud.google.com/go/trace v1.11.1/go.mod h1:IQKNQuBzH72EGaXEodKlNJrWykGZxet2zgjtS60OtjA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/aws/aws-sdk-go-v2 v1.27.2 h1:pLsTXqX93rimAOZG2FIYraDQstZaaGVVN4tNw65v0h8= -github.com/aws/aws-sdk-go-v2 v1.27.2/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 h1:x6xsQXGSmW6frevwDA+vi/wqhp1ct18mVXYN08/93to= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2/go.mod h1:lPprDr1e6cJdyYeGXnRaJoP4Md+cDBvi2eOj00BlGmg= -github.com/aws/aws-sdk-go-v2/config v1.27.18 h1:wFvAnwOKKe7QAyIxziwSKjmer9JBMH1vzIL6W+fYuKk= -github.com/aws/aws-sdk-go-v2/config v1.27.18/go.mod h1:0xz6cgdX55+kmppvPm2IaKzIXOheGJhAufacPJaXZ7c= -github.com/aws/aws-sdk-go-v2/credentials v1.17.18 h1:D/ALDWqK4JdY3OFgA2thcPO1c9aYTT5STS/CvnkqY1c= -github.com/aws/aws-sdk-go-v2/credentials v1.17.18/go.mod h1:JuitCWq+F5QGUrmMPsk945rop6bB57jdscu+Glozdnc= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.5 h1:dDgptDO9dxeFkXy+tEgVkzSClHZje/6JkPW5aZyEvrQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.5/go.mod h1:gjvE2KBUgUQhcv89jqxrIxH9GaKs1JbZzWejj/DaHGA= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.9 h1:cy8ahBJuhtM8GTTSyOkfy6WVPV1IE+SS5/wfXUYuulw= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.9/go.mod h1:CZBXGLaJnEZI6EVNcPd7a6B5IC5cA/GkRWtu9fp3S6Y= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.9 h1:A4SYk07ef04+vxZToz9LWvAXl9LW0NClpPpMsi31cz0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.9/go.mod h1:5jJcHuwDagxN+ErjQ3PU3ocf6Ylc/p9x+BLO/+X4iXw= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.9 h1:vHyZxoLVOgrI8GqX7OMHLXp4YYoxeEsrjweXKpye+ds= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.9/go.mod h1:z9VXZsWA2BvZNH1dT0ToUYwMu/CR9Skkj/TBX+mceZw= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.11 h1:4vt9Sspk59EZyHCAEMaktHKiq0C09noRTQorXD/qV+s= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.11/go.mod h1:5jHR79Tv+Ccq6rwYh+W7Nptmw++WiFafMfR42XhwNl8= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.11 h1:o4T+fKxA3gTMcluBNZZXE9DNaMkJuUL1O3mffCUjoJo= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.11/go.mod h1:84oZdJ+VjuJKs9v1UTC9NaodRZRseOXCTgku+vQJWR8= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.9 h1:TE2i0A9ErH1YfRSvXfCr2SQwfnqsoJT9nPQ9kj0lkxM= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.9/go.mod h1:9TzXX3MehQNGPwCZ3ka4CpwQsoAMWSF48/b+De9rfVM= -github.com/aws/aws-sdk-go-v2/service/kinesis v1.27.10 h1:lmp5qBDoJCLsPwKrYNe6zbHnNvW5jzz/xS+H0jkoSYg= -github.com/aws/aws-sdk-go-v2/service/kinesis v1.27.10/go.mod h1:CUWfw8B25XToRN7+sg092F9Ywjvz0PT4veHXBQ2KE0A= -github.com/aws/aws-sdk-go-v2/service/s3 v1.55.1 h1:UAxBuh0/8sFJk1qOkvOKewP5sWeWaTPDknbQz0ZkDm0= -github.com/aws/aws-sdk-go-v2/service/s3 v1.55.1/go.mod h1:hWjsYGjVuqCgfoveVcVFPXIWgz0aByzwaxKlN1StKcM= -github.com/aws/aws-sdk-go-v2/service/securityhub v1.49.2 h1:ybKzmQRXvLkQ9rb251QPmaC5ZlCK1g8b1MLq7DD5eaE= -github.com/aws/aws-sdk-go-v2/service/securityhub v1.49.2/go.mod h1:6SQ5lQJXJZ4HL8ewgW7kp68UkqQtUE/3UmEvDLpJxKk= -github.com/aws/aws-sdk-go-v2/service/sso v1.20.11 h1:gEYM2GSpr4YNWc6hCd5nod4+d4kd9vWIAWrmGuLdlMw= -github.com/aws/aws-sdk-go-v2/service/sso v1.20.11/go.mod h1:gVvwPdPNYehHSP9Rs7q27U1EU+3Or2ZpXvzAYJNh63w= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.5 h1:iXjh3uaH3vsVcnyZX7MqCoCfcyxIrVE9iOQruRaWPrQ= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.5/go.mod h1:5ZXesEuy/QcO0WUnt+4sDkxhdXRHTu2yG0uCSH8B6os= -github.com/aws/aws-sdk-go-v2/service/sts v1.28.12 h1:M/1u4HBpwLuMtjlxuI2y6HoVLzF5e2mfxHCg7ZVMYmk= -github.com/aws/aws-sdk-go-v2/service/sts v1.28.12/go.mod h1:kcfd+eTdEi/40FIbLq4Hif3XMXnl5b/+t/KTfLt9xIk= -github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= -github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.2 h1:cZpsGsWTIFKymTA0je7IIvi1O7Es7apb9CF3EQlOcfE= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.2/go.mod h1:itPGVDKf9cC/ov4MdvJ2QZ0khw4bfoo9jzwTJlaxy2k= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.2 h1:RopCq1mZTydpZpWfeYDvsnKR5L8VeaNt5JR5wiMfh7Q= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.2/go.mod h1:tlLrnqq33OLuNnYbqswyI5ckZ0QjuM2DFIuaraxxDEU= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.48.2 h1:ffI2ensdT33alWXmBDi/7cvCV7K3o7TF5oE44g8tiN0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.48.2/go.mod h1:pNP/L2wDlaQnQlFvkDKGSruDoYRpmAxB6drgsskfYwg= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.2 h1:th/AQTVtV5u0WVQln/ks+jxhkZ433MeOevmka55fkeg= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.2/go.mod h1:wRbFgBQUVm1YXrvWKofAEmq9HNJTDphbAaJSSX01KUI= +github.com/atc0005/go-teams-notify/v2 v2.13.0 h1:nbDeHy89NjYlF/PEfLVF6lsserY9O5SnN1iOIw3AxXw= +github.com/atc0005/go-teams-notify/v2 v2.13.0/go.mod h1:WSv9moolRsBcpZbwEf6gZxj7h0uJlJskJq5zkEWKO8Y= +github.com/aws/aws-sdk-go-v2 v1.32.0 h1:GuHp7GvMN74PXD5C97KT5D87UhIy4bQPkflQKbfkndg= +github.com/aws/aws-sdk-go-v2 v1.32.0/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 h1:pT3hpW0cOHRJx8Y0DfJUEQuqPild8jRGmSFmBgvydr0= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6/go.mod h1:j/I2++U0xX+cr44QjHay4Cvxj6FUbnxrgmqN3H1jTZA= +github.com/aws/aws-sdk-go-v2/config v1.27.41 h1:esG3WpmEuNJ6F4kVFLumN8nCfA5VBav1KKb3JPx83O4= +github.com/aws/aws-sdk-go-v2/config v1.27.41/go.mod h1:haUg09ebP+ClvPjU3EB/xe0HF9PguO19PD2fdjM2X14= +github.com/aws/aws-sdk-go-v2/credentials v1.17.39 h1:tmVexAhoGqJxNE2oc4/SJqL+Jz1x1iCPt5ts9XcqZCU= +github.com/aws/aws-sdk-go-v2/credentials v1.17.39/go.mod h1:zgOdbDI9epE608PdboJ87CYvPIejAgFevazeJW6iauQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.15 h1:kGjlNc2IXXcxPDcfMyCshNCjVgxUhC/vTJv7NvC9wKk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.15/go.mod h1:rk/HmqPo+dX0Uv0Q1+4w3QKFdICEGSsTYz1hRWvH8UI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.19 h1:Q/k5wCeJkSWs+62kDfOillkNIJ5NqmE3iOfm48g/W8c= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.19/go.mod h1:Wns1C66VvtA2Bv/cUBuKZKQKdjo7EVMhp90aAa+8oTI= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.19 h1:AYLE0lUfKvN6icFTR/p+NmD1amYKTbqHQ1Nm+jwE6BM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.19/go.mod h1:1giLakj64GjuH1NBzF/DXqly5DWHtMTaOzRZ53nFX0I= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.19 h1:FKdiFzTxlTRO71p0C7VrLbkkdW8qfMKF5+ej6bTmkT0= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.19/go.mod h1:abO3pCj7WLQPTllnSeYImqFfkGrmJV0JovWo/gqT5N0= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.0 h1:FQNWhRuSq8QwW74GtU0MrveNhZbqvHsA4dkA9w8fTDQ= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.0/go.mod h1:j/zZ3zmWfGCK91K73YsfHP53BSTLSjL/y6YN39XbBLM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.0 h1:AdbiDUgQZmM28rDIZbiSwFxz8+3B94aOXxzs6oH+EA0= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.0/go.mod h1:uV476Bd80tiDTX4X2redMtagQUg65aU/gzPojSJ4kSI= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.0 h1:1NKXS8XfhMM0bg5wVYa/eOH8AM2f6JijugbKEyQFTIg= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.0/go.mod h1:ph931DUfVfgrhZR7py9olSvHCiRpvaGxNvlWBcXxFds= +github.com/aws/aws-sdk-go-v2/service/kinesis v1.32.0 h1:rWB8cisa9lg89UHbxa9v0tmL6O8onwSf5FndG+5KQ9o= +github.com/aws/aws-sdk-go-v2/service/kinesis v1.32.0/go.mod h1:yG/J6nKcsB5h/MDfcz4LBd4DaZ6v6bhps8gbjLe0tR8= +github.com/aws/aws-sdk-go-v2/service/s3 v1.65.0 h1:2dSm7frMrw2tdJ0QvyccQNJyPGaP24dyDgZ6h1QJMGU= +github.com/aws/aws-sdk-go-v2/service/s3 v1.65.0/go.mod h1:4XSVpw66upN8wND3JZA29eXl2NOZvfFVq7DIP6xvfuQ= +github.com/aws/aws-sdk-go-v2/service/securityhub v1.54.0 h1:2aYy17RgJLQ/7byuUnupyG1zwOa/meg7eTQOP4NWBRM= +github.com/aws/aws-sdk-go-v2/service/securityhub v1.54.0/go.mod h1:DxwMa8+xM898UzufTrwJ52dfrA0iruEmdX/e12k/sxY= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.0 h1:71FvP6XFj53NK+YiAEGVzeiccLVeFnHOCvMig0zOHsE= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.0/go.mod h1:UVJqtKXSd9YppRKgdBIkyv7qgbSGv5DchM3yX0BN2mU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.0 h1:Uco4o19bi3AmBapImNzuMk+rfzlui52BDyVK1UfJeRA= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.0/go.mod h1:+HLFhCpnG08hBee8bUdfd1mBK+rFKPt4O5igR9lXDfk= +github.com/aws/aws-sdk-go-v2/service/sts v1.32.0 h1:GiQUjZM2KUZX68o/LpZ1xqxYMuvoxpRrOwYARYog3vc= +github.com/aws/aws-sdk-go-v2/service/sts v1.32.0/go.mod h1:dKnu7M4MAS2SDlng1ytxd03H+y0LoUfEQ5E2VaaSw/4= +github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= +github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU= +github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM= +github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= +github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI= +github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapwQtU84iWk= -github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= +github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.13.0 h1:HzkeUz1Knt+3bK+8LG1bxOO/jzWZmdxpwC51i202les= +github.com/envoyproxy/go-control-plane v0.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnvMg4d7nvT/wl9WgVXn3Q8= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= -github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM= +github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4= +github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4= +github.com/gin-contrib/gzip v1.0.1 h1:HQ8ENHODeLY7a4g1Au/46Z92bdGFl74OhxcZble9WJE= +github.com/gin-contrib/gzip v1.0.1/go.mod h1:njt428fdUNRvjuJf16tZMYZ2Yl+WQB53X5wmhDwXvC4= +github.com/gin-contrib/pprof v1.5.0 h1:E/Oy7g+kNw94KfdCy3bZxQFtyDnAX2V7axRS7sNYVrU= +github.com/gin-contrib/pprof v1.5.0/go.mod h1:GqFL6LerKoCQ/RSWnkYczkTJ+tOAUVN/8sbnEtaqOKs= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-contrib/zap v1.1.4 h1:xvxTybg6XBdNtcQLH3Tf0lFr4vhDkwzgLLrIGlNTqIo= +github.com/gin-contrib/zap v1.1.4/go.mod h1:7lgEpe91kLbeJkwBTPgtVBy4zMa6oSBEcvj662diqKQ= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= @@ -93,14 +144,25 @@ github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= +github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -127,25 +189,29 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= -github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= -github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= +github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= +github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= +github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= -github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= -github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA= -github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= +github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= +github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= +github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s= +github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= @@ -164,18 +230,33 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0= +github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kyverno/go-wildcard v1.0.5 h1:QTgNRRUFOGz96AB8xmI6ErDwDqS9noKzYppvIC05SRE= github.com/kyverno/go-wildcard v1.0.5/go.mod h1:sZkBvzy+au8C1uiqOH+SdN4psOL+0nhfWgsZzzJKwbs= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= -github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -189,54 +270,62 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= -github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= -github.com/onsi/gomega v1.31.0 h1:54UJxxj6cPInHS3a35wm6BK/F9nHYueZ1NVujHDrnXE= -github.com/onsi/gomega v1.31.0/go.mod h1:DW9aCi7U6Yi40wNVAvT6kzFnEVEI5n3DloYBiKiT6zk= +github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= +github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= +github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= +github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= -github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg= -github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= -github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= +github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= +github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.52.3 h1:5f8uj6ZwHSscOGNdIQg6OiZv/ybiK2CO2q2drVZAQSA= -github.com/prometheus/common v0.52.3/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U= -github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o= -github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/prometheus/common v0.60.0 h1:+V9PAREWNvJMAuJ1x1BaWl9dewMW4YrHZQbx0sJNllA= +github.com/prometheus/common v0.60.0/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4= +github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= +github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM= github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY= +github.com/slack-go/slack v0.14.0 h1:6c0UTfbRnvRssZUsZ2qe0Iu07VAMPjRqOa6oX8ewF4k= +github.com/slack-go/slack v0.14.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= -github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= -github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -250,59 +339,73 @@ github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GH github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns= github.com/toorop/go-dkim v0.0.0-20240103092955-90b7d1423f92 h1:flbMkdl6HxQkLs6DDhH1UkcnFpNBOu70391STjMS0O4= github.com/toorop/go-dkim v0.0.0-20240103092955-90b7d1423f92/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns= -github.com/uptrace/bun v1.2.1 h1:2ENAcfeCfaY5+2e7z5pXrzFKy3vS8VXvkCag6N2Yzfk= -github.com/uptrace/bun v1.2.1/go.mod h1:cNg+pWBUMmJ8rHnETgf65CEvn3aIKErrwOD6IA8e+Ec= -github.com/uptrace/bun/dialect/mysqldialect v1.2.1 h1:tapGyK0VMbpwtmfAZFG0s2GrjX77EduweWEdID2Yigk= -github.com/uptrace/bun/dialect/mysqldialect v1.2.1/go.mod h1:H4ekLaXSXo4TKOVfT9J/yhOvootl1vsBnyRyyUlRVoA= -github.com/uptrace/bun/dialect/pgdialect v1.2.1 h1:ceP99r03u+s8ylaDE/RzgcajwGiC76Jz3nS2ZgyPQ4M= -github.com/uptrace/bun/dialect/pgdialect v1.2.1/go.mod h1:mv6B12cisvSc6bwKm9q9wcrr26awkZK8QXM+nso9n2U= -github.com/uptrace/bun/dialect/sqlitedialect v1.2.1 h1:IprvkIKUjEjvt4VKpcmLpbMIucjrsmUPJOSlg19+a0Q= -github.com/uptrace/bun/dialect/sqlitedialect v1.2.1/go.mod h1:mMQf4NUpgY8bnOanxGmxNiHCdALOggS4cZ3v63a9D/o= -github.com/uptrace/bun/driver/pgdriver v1.2.1 h1:Cp6c1tKzbTIyL8o0cGT6cOhTsmQZdsUNhgcV51dsmLU= -github.com/uptrace/bun/driver/pgdriver v1.2.1/go.mod h1:jEd3WGx74hWLat3/IkesOoWNjrFNUDADK3nkyOFOOJM= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/uptrace/bun v1.2.3 h1:6KDc6YiNlXde38j9ATKufb8o7MS8zllhAOeIyELKrk0= +github.com/uptrace/bun v1.2.3/go.mod h1:8frYFHrO/Zol3I4FEjoXam0HoNk+t5k7aJRl3FXp0mk= +github.com/uptrace/bun/dialect/mysqldialect v1.2.3 h1:DvoXYApIs7NldTv/PVkyalnPe4l0Dax9g4GxeJ81IXM= +github.com/uptrace/bun/dialect/mysqldialect v1.2.3/go.mod h1:F1yEDex5Hu8u4OFQQVSRJBI6IR3ZjEjA8441oyQWoJw= +github.com/uptrace/bun/dialect/pgdialect v1.2.3 h1:YyCxxqeL0lgFWRZzKCOt6mnxUsjqITcxSo0mLqgwMUA= +github.com/uptrace/bun/dialect/pgdialect v1.2.3/go.mod h1:Vx9TscyEq1iN4tnirn6yYGwEflz0KG3rBZTBCLpKAjc= +github.com/uptrace/bun/dialect/sqlitedialect v1.2.3 h1:gCxqT9pFpZxc6iRokdS6QrPF894ycBLxnh/3m7qQeQ0= +github.com/uptrace/bun/dialect/sqlitedialect v1.2.3/go.mod h1:eNiDNdfChKUpPZUTDivb/YvWGvHVsVhCBwDCQ0PvtR8= +github.com/uptrace/bun/driver/pgdriver v1.2.3 h1:VA5TKB0XW7EtreQq2R8Qu/vCAUX2ECaprxGKI9iDuDE= +github.com/uptrace/bun/driver/pgdriver v1.2.3/go.mod h1:yDiYTZYd4FfXFtV01m4I/RkI33IGj9N254jLStaeJLs= +github.com/uptrace/bun/extra/bundebug v1.2.3 h1:2QBykz9/u4SkN9dnraImDcbrMk2fUhuq2gL6hkh9qSc= +github.com/uptrace/bun/extra/bundebug v1.2.3/go.mod h1:bihsYJxXxWZXwc1R3qALTHvp+npE0ElgaCvcjzyPPdw= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xhit/go-simple-mail/v2 v2.16.0 h1:ouGy/Ww4kuaqu2E2UrDw7SvLaziWTB60ICLkIkNVccA= github.com/xhit/go-simple-mail/v2 v2.16.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.50.0 h1:zvpPXY7RfYAGSdYQLjp6zxdJNSYD/+FFoCTQN9IPxBs= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.50.0/go.mod h1:BMn8NB1vsxTljvuorms2hyOs8IBuuBEq0pl7ltOfy30= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0 h1:cEPbyTSEHlQR89XVlyo78gqluF8Y3oMeBkXGWzQsfXY= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0/go.mod h1:DKdbWcT4GH1D0Y3Sqt/PFXt2naRKDWtU+eE6oLdFNA8= -go.opentelemetry.io/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k= -go.opentelemetry.io/otel v1.25.0/go.mod h1:Wa2ds5NOXEMkCmUou1WA7ZBfLTHWIsp034OVD7AO+Vg= -go.opentelemetry.io/otel/metric v1.25.0 h1:LUKbS7ArpFL/I2jJHdJcqMGxkRdxpPHE0VU/D4NuEwA= -go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s= -go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= -go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= -go.opentelemetry.io/otel/trace v1.25.0 h1:tqukZGLwQYRIFtSQM2u2+yfMVTgGVeqRLPUYx1Dq6RM= -go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I= +go.opentelemetry.io/contrib/detectors/gcp v1.30.0 h1:GF+YVnUeJwOy+Ag2cTEpVZq+r2Tnci42FIiNwA2gjME= +go.opentelemetry.io/contrib/detectors/gcp v1.30.0/go.mod h1:p5Av42vWKPezk67MQwLYZwlo/z6xLnN/upaIyQNWBGg= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0 h1:hCq2hNMwsegUvPzI7sPOvtO9cqyy5GbWt/Ybp2xrx8Q= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0/go.mod h1:LqaApwGx/oUmzsbqxkzuBvyoPpkxk3JQWnqfVrJ3wCA= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI= +go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= +go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= +go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= +go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= +go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE= +go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg= +go.opentelemetry.io/otel/sdk/metric v1.30.0 h1:QJLT8Pe11jyHBHfSAgYH7kEmT24eX792jZO1bo4BXkM= +go.opentelemetry.io/otel/sdk/metric v1.30.0/go.mod h1:waS6P3YqFNzeP01kuo/MBBYqaoBJl7efRQHOaydhy1Y= +go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= +go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4= +golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= -golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6 h1:1wqE9dj9NpSm04INVsJhhEUzhuDVjbcyKH91sVyPATw= +golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -312,32 +415,35 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= -golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= -golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -346,34 +452,34 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= -golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= -google.golang.org/api v0.174.0 h1:zB1BWl7ocxfTea2aQ9mgdzXjnfPySllpPOskdnO+q34= -google.golang.org/api v0.174.0/go.mod h1:aC7tB6j0HR1Nl0ni5ghpx6iLasmAX78Zkh/wgxAAjLg= +google.golang.org/api v0.199.0 h1:aWUXClp+VFJmqE0JPvpZOK3LDQMyFKYIow4etYd9qxs= +google.golang.org/api v0.199.0/go.mod h1:ohG4qSztDJmZdjK/Ar6MhbAmb/Rpi4JHOqagsh90K28= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20240415180920-8c6c420018be h1:g4aX8SUFA8V5F4LrSY5EclyGYw1OZN4HS1jTyjB9ZDc= -google.golang.org/genproto v0.0.0-20240415180920-8c6c420018be/go.mod h1:FeSdT5fk+lkxatqJP38MsUicGqHax5cLtmy/6TAuxO4= -google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be h1:Zz7rLWqp0ApfsR/l7+zSHhY3PMiH2xqgxlfYfAfNpoU= -google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be/go.mod h1:dvdCTIoAGbkWbcIKBniID56/7XHTt6WfxXNMxuziJ+w= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be h1:LG9vZxsWGOmUKieR8wPAUR3u3MpnYFQZROPIMaXh7/A= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/genproto v0.0.0-20240930140551-af27646dc61f h1:mCJ6SGikSxVlt9scCayUl2dMq0msUgmBArqRY6umieI= +google.golang.org/genproto v0.0.0-20240930140551-af27646dc61f/go.mod h1:xtVODtPkMQRUZ4kqOTgp6JrXQrPevvfCSdk4mJtHUbM= +google.golang.org/genproto/googleapis/api v0.0.0-20240930140551-af27646dc61f h1:jTm13A2itBi3La6yTGqn8bVSrc3ZZ1r8ENHlIXBfnRA= +google.golang.org/genproto/googleapis/api v0.0.0-20240930140551-af27646dc61f/go.mod h1:CLGoBuH1VHxAUXVPP8FfPwPEVJB6lz3URE5mY2SuayE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f h1:cUMEy+8oS78BWIH9OWazBkzbr090Od9tWBNtZHkOhf0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= -google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/grpc/stats/opentelemetry v0.0.0-20241004113128-859602c14c6c h1:pJuL8NmW9MVLZijtjSba++KjWp4JSUzTqHYInzgdxwA= +google.golang.org/grpc/stats/opentelemetry v0.0.0-20241004113128-859602c14c6c/go.mod h1:xwT0YrcBcgR1ZSSLJtUgCjF5QlvTOhiwA/I9TcYf3Gg= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -383,11 +489,13 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= @@ -402,20 +510,21 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.30.0 h1:siWhRq7cNjy2iHssOB9SCGNCl2spiF1dO3dABqZ8niA= -k8s.io/api v0.30.0/go.mod h1:OPlaYhoHs8EQ1ql0R/TsUgaRPhpKNxIMrKQfWUp8QSE= -k8s.io/apimachinery v0.30.0 h1:qxVPsyDM5XS96NIh9Oj6LavoVFYff/Pon9cZeDIkHHA= -k8s.io/apimachinery v0.30.0/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= -k8s.io/client-go v0.30.0 h1:sB1AGGlhY/o7KCyCEQ0bPWzYDL0pwOZO4vAtTSh/gJQ= -k8s.io/client-go v0.30.0/go.mod h1:g7li5O5256qe6TYdAMyX/otJqMhIiGgTapdLchhmOaY= -k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= -k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3 h1:SbdLaI6mM6ffDSJCadEaD4IkuPzepLDGlkd2xV0t1uA= -k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= -k8s.io/utils v0.0.0-20240310230437-4693a0247e57 h1:gbqbevonBh57eILzModw6mrkbwM0gQBEuevE/AaBsHY= -k8s.io/utils v0.0.0-20240310230437-4693a0247e57/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -mellium.im/sasl v0.3.1 h1:wE0LW6g7U83vhvxjC1IY8DnXM+EU095yeo8XClvCdfo= -mellium.im/sasl v0.3.1/go.mod h1:xm59PUYpZHhgQ9ZqoJ5QaCqzWMi8IeS49dhp6plPCzw= +k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU= +k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI= +k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U= +k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0= +k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38 h1:1dWzkmJrrprYvjGwh9kEUxmcUV/CtNU8QM7h1FLWQOo= +k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38/go.mod h1:coRQXBK9NxO98XUv3ZD6AK3xzHCxV6+b7lrquKwaKzA= +k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 h1:MDF6h2H/h4tbzmtIKTuctcwZmY0tY9mD9fNT47QO6HI= +k8s.io/utils v0.0.0-20240921022957-49e7df575cb6/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +mellium.im/sasl v0.3.2 h1:PT6Xp7ccn9XaXAnJ03FcEjmAn7kK1x7aoXV6F+Vmrl0= +mellium.im/sasl v0.3.2/go.mod h1:NKXDi1zkr+BlMHLQjY3ofYuU4KSPFxknb8mfEu6SveY= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= diff --git a/manifest/policy-reporter-kyverno-ui-ha/README.md b/manifest/policy-reporter-kyverno-ui-ha/README.md index b229cc22..c325e60c 100644 --- a/manifest/policy-reporter-kyverno-ui-ha/README.md +++ b/manifest/policy-reporter-kyverno-ui-ha/README.md @@ -8,7 +8,7 @@ Seet the [Documentation](https://kyverno.github.io/policy-reporter/core/config-r # send pushes to the Policy Reporter UI ui: host: http://policy-reporter-ui:8082 - minimumPriority: "warning" + minimumSeverity: "medium" skipExistingOnStartup: true # (optional) cache results in an central, external redis diff --git a/pkg/api/basic_auth.go b/pkg/api/basic_auth.go deleted file mode 100644 index 4c05f413..00000000 --- a/pkg/api/basic_auth.go +++ /dev/null @@ -1,41 +0,0 @@ -package api - -import ( - "crypto/sha256" - "crypto/subtle" - "net/http" -) - -type BasicAuth struct { - Username string - Password string -} - -func HTTPBasic(auth *BasicAuth, next http.HandlerFunc) http.HandlerFunc { - if auth == nil { - return next - } - - expectedUsernameHash := sha256.Sum256([]byte(auth.Username)) - expectedPasswordHash := sha256.Sum256([]byte(auth.Password)) - - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - username, password, ok := r.BasicAuth() - if ok { - - usernameHash := sha256.Sum256([]byte(username)) - passwordHash := sha256.Sum256([]byte(password)) - - usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1) - passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1) - - if usernameMatch && passwordMatch { - next.ServeHTTP(w, r) - return - } - } - - w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) - http.Error(w, "Unauthorized", http.StatusUnauthorized) - }) -} diff --git a/pkg/api/basic_auth_test.go b/pkg/api/basic_auth_test.go deleted file mode 100644 index 13838e3d..00000000 --- a/pkg/api/basic_auth_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package api_test - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/kyverno/policy-reporter/pkg/api" -) - -func Test_HTTPBasicSkipped(t *testing.T) { - handler := api.HTTPBasic(nil, api.HealthzHandler(func() bool { return true })) - - req, err := http.NewRequest("GET", "/healthz", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusUnauthorized) - } -} - -func Test_HTTPBasicUnauthorized(t *testing.T) { - handler := api.HTTPBasic(&api.BasicAuth{Username: "user", Password: "password"}, api.HealthzHandler(func() bool { return true })) - - req, err := http.NewRequest("GET", "/healthz", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusUnauthorized { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusUnauthorized) - } - - if rr.Header().Get("WWW-Authenticate") == "" { - t.Errorf("Missing Header: WWW-Authenticate") - } -} - -func Test_HTTPBasicAuthorized(t *testing.T) { - handler := api.HTTPBasic(&api.BasicAuth{Username: "user", Password: "password"}, api.HealthzHandler(func() bool { return true })) - - req, err := http.NewRequest("GET", "/healthz", nil) - if err != nil { - t.Fatal(err) - } - - req.SetBasicAuth("user", "password") - - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusUnauthorized) - } -} diff --git a/pkg/api/gzip.go b/pkg/api/gzip.go deleted file mode 100644 index 348d9166..00000000 --- a/pkg/api/gzip.go +++ /dev/null @@ -1,50 +0,0 @@ -package api - -import ( - "compress/gzip" - "io" - "net/http" - "strings" - "sync" -) - -var gzPool = sync.Pool{ - New: func() interface{} { - w := gzip.NewWriter(io.Discard) - return w - }, -} - -type gzipResponseWriter struct { - io.Writer - http.ResponseWriter -} - -func (w *gzipResponseWriter) WriteHeader(status int) { - w.Header().Del("Content-Length") - w.ResponseWriter.WriteHeader(status) -} - -func (w *gzipResponseWriter) Write(b []byte) (int, error) { - return w.Writer.Write(b) -} - -// Gzip middleware for HTTP Handler -func Gzip(next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { - next.ServeHTTP(w, r) - return - } - - w.Header().Set("Content-Encoding", "gzip") - - gz := gzPool.Get().(*gzip.Writer) - defer gzPool.Put(gz) - - gz.Reset(w) - defer gz.Close() - - next(&gzipResponseWriter{ResponseWriter: w, Writer: gz}, r) - } -} diff --git a/pkg/api/gzip_test.go b/pkg/api/gzip_test.go deleted file mode 100644 index 6430c274..00000000 --- a/pkg/api/gzip_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package api_test - -import ( - "bytes" - "compress/gzip" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/kyverno/policy-reporter/pkg/api" - v1 "github.com/kyverno/policy-reporter/pkg/api/v1" - "github.com/kyverno/policy-reporter/pkg/target" -) - -func Test_GzipCompression(t *testing.T) { - handl := v1.NewHandler(nil) - - t.Run("GzipRespose", func(t *testing.T) { - req, err := http.NewRequest("GET", "/targets", nil) - if err != nil { - t.Fatal(err) - } - req.Header.Add("Accept-Encoding", "gzip") - - rr := httptest.NewRecorder() - handler := api.Gzip(handl.TargetsHandler(make([]target.Client, 0))) - - handler.ServeHTTP(rr, req) - - reader, _ := gzip.NewReader(rr.Body) - defer reader.Close() - - buf := new(bytes.Buffer) - buf.ReadFrom(reader) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - - expected := "[]" - if !strings.Contains(buf.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", buf.String(), expected) - } - }) - t.Run("Uncompressed Respose", func(t *testing.T) { - req, err := http.NewRequest("GET", "/targets", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler := api.Gzip(handl.TargetsHandler(make([]target.Client, 0))) - - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - - expected := "[]" - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) - } - }) - - t.Run("Uncompressed Respose", func(t *testing.T) { - req, err := http.NewRequest("GET", "/targets", nil) - if err != nil { - t.Fatal(err) - } - req.Header.Add("Accept-Encoding", "gzip") - - rr := httptest.NewRecorder() - handler := api.Gzip(func(w http.ResponseWriter, req *http.Request) { - w.WriteHeader(204) - }) - - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusNoContent { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - }) -} diff --git a/pkg/api/handler.go b/pkg/api/handler.go deleted file mode 100644 index 6bf5a7f8..00000000 --- a/pkg/api/handler.go +++ /dev/null @@ -1,49 +0,0 @@ -package api - -import ( - "fmt" - "net/http" - - "go.uber.org/zap" -) - -// HealthzHandler for the Halthz REST API -func HealthzHandler(synced func() bool) http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - if !synced() { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusServiceUnavailable) - - fmt.Fprint(w, `{ "error": "Informers not in sync" }`) - - zap.L().Warn("informers not synced yet, waiting for k8s client to complete startup") - - return - } - - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) - - fmt.Fprint(w, "{}") - } -} - -// ReadyHandler for the Halthz REST API -func ReadyHandler(synced func() bool) http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - if !synced() { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusServiceUnavailable) - - fmt.Fprint(w, `{ "error": "Informers not in sync" }`) - - zap.L().Warn("informers not synced yet, waiting for k8s client to be up") - - return - } - - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) - fmt.Fprint(w, "{}") - } -} diff --git a/pkg/api/handler_test.go b/pkg/api/handler_test.go deleted file mode 100644 index 883cf50a..00000000 --- a/pkg/api/handler_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package api_test - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/kyverno/policy-reporter/pkg/api" -) - -func Test_HealthzAPI(t *testing.T) { - t.Run("Respose", func(t *testing.T) { - req, err := http.NewRequest("GET", "/healthz", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler := api.HealthzHandler(func() bool { return true }) - - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - }) - t.Run("Unavailable Response", func(t *testing.T) { - req, err := http.NewRequest("GET", "/healthz", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler := api.HealthzHandler(func() bool { return false }) - - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusServiceUnavailable { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusServiceUnavailable) - } - }) -} - -func Test_ReadyAPI(t *testing.T) { - t.Run("Success Response", func(t *testing.T) { - req, err := http.NewRequest("GET", "/ready", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler := api.ReadyHandler(func() bool { return true }) - - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - }) - t.Run("Unavailable Response", func(t *testing.T) { - req, err := http.NewRequest("GET", "/ready", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler := api.ReadyHandler(func() bool { return false }) - - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusServiceUnavailable { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusServiceUnavailable) - } - }) -} diff --git a/pkg/api/healthz.go b/pkg/api/healthz.go new file mode 100644 index 00000000..36254e5e --- /dev/null +++ b/pkg/api/healthz.go @@ -0,0 +1,25 @@ +package api + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type HealthCheck = func() error + +// HealthzHandler for the Halthz REST API +func HealthzHandler(checks []HealthCheck) gin.HandlerFunc { + return func(ctx *gin.Context) { + for _, c := range checks { + if err := c(); err != nil { + zap.L().Warn("health check failed", zap.Error(err)) + ctx.AbortWithError(http.StatusServiceUnavailable, err) + return + } + } + + ctx.JSON(http.StatusOK, gin.H{}) + } +} diff --git a/pkg/api/healthz_test.go b/pkg/api/healthz_test.go new file mode 100644 index 00000000..042394f0 --- /dev/null +++ b/pkg/api/healthz_test.go @@ -0,0 +1,71 @@ +package api_test + +import ( + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + + "github.com/kyverno/policy-reporter/pkg/api" +) + +func TestHealthCheckSuccess(t *testing.T) { + check := func() error { + return nil + } + + gin.SetMode(gin.ReleaseMode) + + server := api.NewServer(gin.New(), api.WithHealthChecks([]api.HealthCheck{check})) + + req, _ := http.NewRequest("GET", "/healthz", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + assert := assert.New(t) + assert.Equal(http.StatusOK, w.Code) +} + +func TestHealthCheckError(t *testing.T) { + check := func() error { + return nil + } + + err := func() error { + return errors.New("unhealthy") + } + + gin.SetMode(gin.ReleaseMode) + + server := api.NewServer(gin.New(), api.WithHealthChecks([]api.HealthCheck{check, err})) + + req, _ := http.NewRequest("GET", "/healthz", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + assert := assert.New(t) + assert.Equal(http.StatusServiceUnavailable, w.Code) +} + +func TestReadyCheckSuccess(t *testing.T) { + check := func() error { + return nil + } + + gin.SetMode(gin.ReleaseMode) + + server := api.NewServer(gin.New(), api.WithHealthChecks([]api.HealthCheck{check})) + + req, _ := http.NewRequest("GET", "/ready", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + assert := assert.New(t) + assert.Equal(http.StatusOK, w.Code) +} diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go new file mode 100644 index 00000000..2bf0e12d --- /dev/null +++ b/pkg/api/metrics.go @@ -0,0 +1,14 @@ +package api + +import ( + "github.com/gin-gonic/gin" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +func MetricsHandler() gin.HandlerFunc { + h := promhttp.Handler() + + return func(c *gin.Context) { + h.ServeHTTP(c.Writer, c.Request) + } +} diff --git a/pkg/api/metrics_test.go b/pkg/api/metrics_test.go new file mode 100644 index 00000000..fffc8186 --- /dev/null +++ b/pkg/api/metrics_test.go @@ -0,0 +1,61 @@ +package api_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + + "github.com/kyverno/policy-reporter/pkg/api" +) + +func TestMetrics(t *testing.T) { + gin.SetMode(gin.ReleaseMode) + + server := api.NewServer(gin.New(), api.WithMetrics()) + + req, _ := http.NewRequest("GET", "/metrics", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + assert := assert.New(t) + assert.Equal(http.StatusOK, w.Code) +} + +func TestMetricsWithBasicAuthError(t *testing.T) { + gin.SetMode(gin.ReleaseMode) + + server := api.NewServer(gin.New(), api.WithBasicAuth(api.BasicAuth{ + Username: "user", + Password: "password", + }), api.WithMetrics()) + + req, _ := http.NewRequest("GET", "/metrics", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + assert := assert.New(t) + assert.Equal(http.StatusUnauthorized, w.Code) +} + +func TestMetricsWithBasicAuthSuccess(t *testing.T) { + gin.SetMode(gin.ReleaseMode) + + server := api.NewServer(gin.New(), api.WithBasicAuth(api.BasicAuth{ + Username: "user", + Password: "password", + }), api.WithMetrics()) + + req, _ := http.NewRequest("GET", "/metrics", nil) + req.SetBasicAuth("user", "password") + w := httptest.NewRecorder() + + server.Serve(w, req) + + assert := assert.New(t) + assert.Equal(http.StatusOK, w.Code) +} diff --git a/pkg/api/server.go b/pkg/api/server.go index 02f3a442..775e7de9 100644 --- a/pkg/api/server.go +++ b/pkg/api/server.go @@ -1,167 +1,125 @@ package api import ( - "context" "fmt" "net/http" - pprof "net/http/pprof" + "time" - "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/gin-contrib/gzip" + "github.com/gin-contrib/pprof" + ginzap "github.com/gin-contrib/zap" + "github.com/gin-gonic/gin" "go.uber.org/zap" - "go.uber.org/zap/zapcore" - - v1 "github.com/kyverno/policy-reporter/pkg/api/v1" - "github.com/kyverno/policy-reporter/pkg/email/violations" - "github.com/kyverno/policy-reporter/pkg/target" ) -// Server for the Lifecycle and optional HTTP REST API -type Server interface { - // Start the HTTP Server - Start() error - // Shutdown the HTTP Sever - Shutdown(ctx context.Context) error - // RegisterLifecycleHandler adds healthy and readiness APIs - RegisterLifecycleHandler() - // RegisterMetricsHandler adds the optional metrics endpoint - RegisterMetricsHandler() - // RegisterV1Handler adds the optional v1 REST APIs - RegisterV1Handler(v1.PolicyReportFinder, *violations.Reporter) - // RegisterProfilingHandler adds the optional pprof profiling APIs - RegisterProfilingHandler() +type ServerOption func(s *Server) error + +type Handler interface { + Register(group *gin.RouterGroup) error } -type httpServer struct { - http http.Server - mux *http.ServeMux - targets []target.Client - synced func() bool - logger *zap.Logger - auth *BasicAuth +type BasicAuth struct { + Username string + Password string } -func (s *httpServer) middleware(handler http.HandlerFunc) http.HandlerFunc { - handler = Gzip(handler) +type Server struct { + middleware []gin.HandlerFunc + engine *gin.Engine + port int +} - if s.auth != nil { - handler = HTTPBasic(s.auth, handler) +func (s *Server) Start() error { + return s.engine.Run(fmt.Sprintf(":%d", s.port)) +} + +func (s *Server) Serve(w http.ResponseWriter, req *http.Request) { + s.engine.ServeHTTP(w, req) +} + +func (s *Server) Register(path string, handler Handler) error { + return handler.Register(s.engine.Group(path, s.middleware...)) +} + +func NewServer(engine *gin.Engine, options ...ServerOption) *Server { + server := &Server{ + engine: engine, + port: 8080, } - return handler -} - -func (s *httpServer) RegisterLifecycleHandler() { - s.mux.HandleFunc("/healthz", HealthzHandler(s.synced)) - s.mux.HandleFunc("/ready", ReadyHandler(s.synced)) -} - -func (s *httpServer) RegisterV1Handler(finder v1.PolicyReportFinder, reporter *violations.Reporter) { - handler := v1.NewHandler(finder) - - s.mux.HandleFunc("/v1/targets", s.middleware(handler.TargetsHandler(s.targets))) - s.mux.HandleFunc("/v1/namespaces", s.middleware(handler.NamespaceListHandler())) - s.mux.HandleFunc("/v1/rule-status-count", s.middleware(handler.RuleStatusCountHandler())) - - s.mux.HandleFunc("/v1/policy-reports", s.middleware(handler.PolicyReportListHandler())) - s.mux.HandleFunc("/v1/cluster-policy-reports", s.middleware(handler.ClusterPolicyReportListHandler())) - - s.mux.HandleFunc("/v1/namespaced-resources/categories", s.middleware(handler.NamespacedCategoryListHandler())) - s.mux.HandleFunc("/v1/namespaced-resources/policies", s.middleware(handler.NamespacedResourcesPolicyListHandler())) - s.mux.HandleFunc("/v1/namespaced-resources/rules", s.middleware(handler.NamespacedResourcesRuleListHandler())) - s.mux.HandleFunc("/v1/namespaced-resources/kinds", s.middleware(handler.NamespacedResourcesKindListHandler())) - s.mux.HandleFunc("/v1/namespaced-resources/resources", s.middleware(handler.NamespacedResourcesListHandler())) - s.mux.HandleFunc("/v1/namespaced-resources/sources", s.middleware(handler.NamespacedSourceListHandler())) - s.mux.HandleFunc("/v1/namespaced-resources/report-labels", s.middleware(handler.NamespacedReportLabelListHandler())) - s.mux.HandleFunc("/v1/namespaced-resources/status-counts", s.middleware(handler.NamespacedResourcesStatusCountsHandler())) - s.mux.HandleFunc("/v1/namespaced-resources/results", s.middleware(handler.NamespacedResourcesResultHandler())) - - s.mux.HandleFunc("/v1/cluster-resources/policies", s.middleware(handler.ClusterResourcesPolicyListHandler())) - s.mux.HandleFunc("/v1/cluster-resources/rules", s.middleware(handler.ClusterResourcesRuleListHandler())) - s.mux.HandleFunc("/v1/cluster-resources/kinds", s.middleware(handler.ClusterResourcesKindListHandler())) - s.mux.HandleFunc("/v1/cluster-resources/resources", s.middleware(handler.ClusterResourcesListHandler())) - s.mux.HandleFunc("/v1/cluster-resources/sources", Gzip(handler.ClusterResourcesSourceListHandler())) - s.mux.HandleFunc("/v1/cluster-resources/report-labels", s.middleware(handler.ClusterReportLabelListHandler())) - s.mux.HandleFunc("/v1/cluster-resources/status-counts", s.middleware(handler.ClusterResourcesStatusCountHandler())) - s.mux.HandleFunc("/v1/cluster-resources/results", s.middleware(handler.ClusterResourcesResultHandler())) - s.mux.HandleFunc("/v1/cluster-resources/categories", s.middleware(handler.ClusterCategoryListHandler())) - - htmlHandler := v1.NewHTMLHandler(finder, reporter) - - s.mux.HandleFunc("/v1/html-report/violations", s.middleware(htmlHandler.HTMLReport())) -} - -func (s *httpServer) RegisterMetricsHandler() { - handler := promhttp.Handler() - - if s.auth != nil { - s.mux.HandleFunc("/metrics", HTTPBasic(s.auth, handler.ServeHTTP)) - return + for _, opt := range options { + if err := opt(server); err != nil { + zap.L().Error("failed to apply server function", zap.Error(err)) + } } - s.mux.Handle("/metrics", handler) + return server } -func (s *httpServer) RegisterProfilingHandler() { - s.mux.HandleFunc("/debug/pprof/", pprof.Index) - s.mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) - s.mux.HandleFunc("/debug/pprof/profile", pprof.Profile) - s.mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) - s.mux.HandleFunc("/debug/pprof/trace", pprof.Trace) -} +func WithBasicAuth(auth BasicAuth) ServerOption { + return func(s *Server) error { + s.middleware = append(s.middleware, gin.BasicAuth(gin.Accounts{ + auth.Username: auth.Password, + })) -func (s *httpServer) Start() error { - return s.http.ListenAndServe() -} - -func (s *httpServer) Shutdown(ctx context.Context) error { - return s.http.Shutdown(ctx) -} - -// NewServer constructor for a new API Server -func NewServer(targets []target.Client, port int, logger *zap.Logger, auth *BasicAuth, synced func() bool) Server { - mux := http.NewServeMux() - - s := &httpServer{ - targets: targets, - synced: synced, - mux: mux, - logger: logger, - auth: auth, - http: http.Server{ - Addr: fmt.Sprintf(":%d", port), - Handler: NewLoggerMiddleware(logger, mux), - }, + return nil } - - s.RegisterLifecycleHandler() - - return s } -func NewLoggerMiddleware(logger *zap.Logger, mux http.Handler) http.Handler { - if logger == nil { - return mux +func WithHealthChecks(checks []HealthCheck) ServerOption { + return func(s *Server) error { + s.engine.GET("healthz", HealthzHandler(checks)) + s.engine.GET("ready", HealthzHandler(checks)) + + return nil + } +} + +func WithLogging(logger *zap.Logger) ServerOption { + return func(s *Server) error { + s.engine.Use(ginzap.Ginzap(logger, time.RFC3339, true)) + s.engine.Use(ginzap.RecoveryWithZap(logger, true)) + + return nil + } +} + +func WithGZIP() ServerOption { + return func(s *Server) error { + s.engine.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedPaths([]string{"/metrics"}))) + + return nil + } +} + +func WithRecovery() ServerOption { + return func(s *Server) error { + s.engine.Use(gin.Recovery()) + + return nil + } +} + +func WithPort(port int) ServerOption { + return func(s *Server) error { + s.port = port + + return nil + } +} + +func WithProfiling() ServerOption { + return func(s *Server) error { + pprof.Register(s.engine) + + return nil + } +} + +func WithMetrics() ServerOption { + return func(s *Server) error { + s.engine.GET("metrics", append(s.middleware, MetricsHandler())...) + + return nil } - - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fields := []zapcore.Field{ - zap.String("proto", r.Proto), - zap.String("user-agent", r.Header.Get("User-Agent")), - zap.String("path", r.URL.Path), - } - - if query := r.URL.RawQuery; query != "" { - fields = append(fields, zap.String("query", query)) - } - if ref := r.Header.Get("Referer"); ref != "" { - fields = append(fields, zap.String("referer", ref)) - } - if scheme := r.URL.Scheme; scheme != "" { - fields = append(fields, zap.String("scheme", scheme)) - } - - logger.Debug("Serve", fields...) - - mux.ServeHTTP(w, r) - }) } diff --git a/pkg/api/server_test.go b/pkg/api/server_test.go index f44fc15c..4d8299d3 100644 --- a/pkg/api/server_test.go +++ b/pkg/api/server_test.go @@ -1,86 +1,139 @@ package api_test import ( - "context" - "fmt" - "math/rand" "net/http" + "net/http/httptest" "testing" - "time" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" "go.uber.org/zap" "github.com/kyverno/policy-reporter/pkg/api" - "github.com/kyverno/policy-reporter/pkg/target" ) -var logger = zap.NewNop() - -func Test_NewServer(t *testing.T) { - rnd := rand.New(rand.NewSource(time.Now().Unix())).Float64() - if rnd < 0.3 { - rnd += 0.4 - } - - port := int(rnd * 10000) - - server := api.NewServer( - make([]target.Client, 0), - port, - logger, - nil, - func() bool { return true }, - ) - - server.RegisterMetricsHandler() - server.RegisterV1Handler(nil, nil) - server.RegisterProfilingHandler() - - serviceRunning := make(chan struct{}) - serviceDone := make(chan struct{}) - - go func() { - close(serviceRunning) - err := server.Start() - if err != nil { - fmt.Println(err) - } - defer close(serviceDone) - }() - - <-serviceRunning - - client := http.Client{} - - req, err := http.NewRequest("GET", fmt.Sprintf("http://localhost:%d/ready", port), nil) - if err != nil { - return - } - - res, err := client.Do(req) - - server.Shutdown(context.Background()) - if err != nil { - return - } - - if res.StatusCode != http.StatusOK { - t.Errorf("Unexpected Error Code: %d", res.StatusCode) - } - - <-serviceDone +var check = func() error { + return nil } -func Test_SetupServerWithAuth(t *testing.T) { - server := api.NewServer( - make([]target.Client, 0), - 8080, - logger, - &api.BasicAuth{Username: "user", Password: "password"}, - func() bool { return true }, - ) +func TestWithoutGZIP(t *testing.T) { + gin.SetMode(gin.ReleaseMode) - server.RegisterMetricsHandler() - server.RegisterV1Handler(nil, nil) - server.RegisterProfilingHandler() + engine := gin.New() + + server := api.NewServer(engine, api.WithHealthChecks([]api.HealthCheck{check})) + + req, _ := http.NewRequest("GET", "/healthz", nil) + req.Header.Add("Accept-Encoding", "gzip") + w := httptest.NewRecorder() + + server.Serve(w, req) + + assert := assert.New(t) + assert.Equal(http.StatusOK, w.Code) + assert.Equal("", w.Header().Get("Content-Encoding")) +} + +func TestWithGZIP(t *testing.T) { + gin.SetMode(gin.ReleaseMode) + + server := api.NewServer(gin.New(), api.WithGZIP(), api.WithHealthChecks([]api.HealthCheck{check})) + + req, _ := http.NewRequest("GET", "/healthz", nil) + req.Header.Add("Accept-Encoding", "gzip") + w := httptest.NewRecorder() + + server.Serve(w, req) + + assert := assert.New(t) + assert.Equal(http.StatusOK, w.Code) + assert.Equal("gzip", w.Header().Get("Content-Encoding")) +} + +func TestWithProfiling(t *testing.T) { + gin.SetMode(gin.ReleaseMode) + + server := api.NewServer(gin.New(), api.WithProfiling()) + + req, _ := http.NewRequest("GET", "/debug/pprof/", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + assert := assert.New(t) + assert.Equal(http.StatusOK, w.Code) +} + +type testHandler struct{} + +func (h *testHandler) Register(group *gin.RouterGroup) error { + group.GET("", func(ctx *gin.Context) { + ctx.JSON(http.StatusOK, nil) + }) + + return nil +} + +func TestWithCustomHandler(t *testing.T) { + gin.SetMode(gin.ReleaseMode) + + server := api.NewServer(gin.New(), api.WithProfiling()) + server.Register("/test", &testHandler{}) + + req, _ := http.NewRequest("GET", "/test", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + assert := assert.New(t) + assert.Equal(http.StatusOK, w.Code) +} + +func TestWithRecover(t *testing.T) { + gin.SetMode(gin.ReleaseMode) + engine := gin.New() + server := api.NewServer(engine, api.WithRecovery()) + + engine.GET("/recover", func(ctx *gin.Context) { + panic("recover") + }) + + req, _ := http.NewRequest("GET", "/recover", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + assert := assert.New(t) + assert.Equal(http.StatusInternalServerError, w.Code) +} + +func TestWithZapLoggingRecover(t *testing.T) { + gin.SetMode(gin.ReleaseMode) + engine := gin.New() + server := api.NewServer(engine, api.WithLogging(zap.L())) + + engine.GET("/recover", func(ctx *gin.Context) { + panic("recover") + }) + + req, _ := http.NewRequest("GET", "/recover", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + assert := assert.New(t) + assert.Equal(http.StatusInternalServerError, w.Code) +} + +func TestWithPort(t *testing.T) { + gin.SetMode(gin.ReleaseMode) + + server := api.NewServer(gin.New(), api.WithProfiling(), api.WithPort(8082)) + req, _ := http.NewRequest("GET", "/debug/pprof/", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + assert := assert.New(t) + assert.Equal(http.StatusOK, w.Code) } diff --git a/pkg/api/utils.go b/pkg/api/utils.go new file mode 100644 index 00000000..1efb214e --- /dev/null +++ b/pkg/api/utils.go @@ -0,0 +1,103 @@ +package api + +import ( + "net/http" + "strconv" + "strings" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" + + db "github.com/kyverno/policy-reporter/pkg/database" +) + +type Paginated[T any] struct { + Items []T `json:"items"` + Count int `json:"count"` +} + +func SendResponse(ctx *gin.Context, content any, errMsg string, err error) { + if err != nil { + zap.L().Error(errMsg, zap.Error(err)) + ctx.AbortWithError(http.StatusInternalServerError, err) + return + } + + ctx.JSON(http.StatusOK, content) +} + +func BuildFilter(ctx *gin.Context) db.Filter { + labels := map[string]string{} + + for _, label := range ctx.QueryArray("labels") { + parts := strings.Split(label, ":") + if len(parts) != 2 { + continue + } + + labels[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) + } + + exclude := map[string][]string{} + for _, sourceKind := range ctx.QueryArray("exclude") { + parts := strings.Split(sourceKind, ":") + length := len(parts) + if length < 2 { + continue + } + + if l, ok := exclude[strings.TrimSpace(parts[0])]; ok { + exclude[strings.TrimSpace(parts[0])] = append(l, strings.TrimSpace(parts[1])) + } else { + exclude[strings.TrimSpace(parts[0])] = []string{strings.TrimSpace(parts[1])} + } + } + + id := ctx.Query("resource_id") + if id == "" { + id = ctx.Query("id") + } + + return db.Filter{ + Namespaces: ctx.QueryArray("namespaces"), + Kinds: ctx.QueryArray("kinds"), + Resources: ctx.QueryArray("resources"), + Sources: ctx.QueryArray("sources"), + Categories: ctx.QueryArray("categories"), + Severities: ctx.QueryArray("severities"), + Policies: ctx.QueryArray("policies"), + Rules: ctx.QueryArray("rules"), + Status: ctx.QueryArray("status"), + ReportLabel: labels, + Search: ctx.Query("search"), + ResourceID: id, + Exclude: exclude, + Namespaced: ctx.Query("namespaced") == "true", + } +} + +func BuildPagination(ctx *gin.Context, defaultOrder []string) db.Pagination { + page, err := strconv.Atoi(ctx.Query("page")) + if err != nil || page < 1 { + page = 0 + } + offset, err := strconv.Atoi(ctx.Query("offset")) + if err != nil || offset < 1 { + offset = 0 + } + direction := "ASC" + if strings.ToLower(ctx.Query("direction")) == "desc" { + direction = "DESC" + } + sortBy := ctx.QueryArray("sortBy") + if len(sortBy) == 0 { + sortBy = defaultOrder + } + + return db.Pagination{ + Page: page, + Offset: offset, + SortBy: sortBy, + Direction: direction, + } +} diff --git a/pkg/api/utils_test.go b/pkg/api/utils_test.go new file mode 100644 index 00000000..f62e8af4 --- /dev/null +++ b/pkg/api/utils_test.go @@ -0,0 +1,112 @@ +package api_test + +import ( + "errors" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + + "github.com/kyverno/policy-reporter/pkg/api" + db "github.com/kyverno/policy-reporter/pkg/database" +) + +func TestSendResponseSuccess(t *testing.T) { + gin.SetMode(gin.ReleaseMode) + engine := gin.New() + server := api.NewServer(engine, api.WithRecovery()) + + engine.GET("/send", func(ctx *gin.Context) { + api.SendResponse(ctx, "data", "", nil) + }) + + req, _ := http.NewRequest("GET", "/send", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + assert := assert.New(t) + assert.Equal(http.StatusOK, w.Code) + assert.Equal(`"data"`, string(w.Body.Bytes())) +} + +func TestSendResponseError(t *testing.T) { + gin.SetMode(gin.ReleaseMode) + engine := gin.New() + server := api.NewServer(engine, api.WithRecovery()) + + engine.GET("/send", func(ctx *gin.Context) { + api.SendResponse(ctx, nil, "errorMsg", errors.New("error")) + }) + + req, _ := http.NewRequest("GET", "/send", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + assert := assert.New(t) + assert.Equal(http.StatusInternalServerError, w.Code) + assert.Equal("", string(w.Body.Bytes())) +} + +func TestBuildFilter(t *testing.T) { + filter := api.BuildFilter(&gin.Context{ + Request: &http.Request{ + URL: &url.URL{ + RawQuery: "labels=env:test&labels=app:nginx&labels=invalid&exclude=kyverno:Pod&exclude=kyverno:Job&exclude=kyverno&status=pass&namespaced=true", + }, + }, + }) + + assert := assert.New(t) + assert.Equal(db.Filter{ + ReportLabel: map[string]string{ + "env": "test", + "app": "nginx", + }, + Exclude: map[string][]string{ + "kyverno": {"Pod", "Job"}, + }, + Status: []string{"pass"}, + Namespaced: true, + }, filter) +} + +func TestPaginationDefaults(t *testing.T) { + pagination := api.BuildPagination(&gin.Context{ + Request: &http.Request{ + URL: &url.URL{ + RawQuery: "", + }, + }, + }, []string{"namespace", "source"}) + + assert := assert.New(t) + assert.Equal(db.Pagination{ + Page: 0, + Offset: 0, + SortBy: []string{"namespace", "source"}, + Direction: "ASC", + }, pagination) +} + +func TestPaginationFromURL(t *testing.T) { + pagination := api.BuildPagination(&gin.Context{ + Request: &http.Request{ + URL: &url.URL{ + RawQuery: "page=5&offset=10&direction=desc&sortBy=namespace&sortBy=kind", + }, + }, + }, []string{"namespace", "source"}) + + assert := assert.New(t) + assert.Equal(db.Pagination{ + Page: 5, + Offset: 10, + SortBy: []string{"namespace", "kind"}, + Direction: "DESC", + }, pagination) +} diff --git a/pkg/api/v1/api.go b/pkg/api/v1/api.go new file mode 100644 index 00000000..f1584eae --- /dev/null +++ b/pkg/api/v1/api.go @@ -0,0 +1,267 @@ +package v1 + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" + + "github.com/kyverno/policy-reporter/pkg/api" + db "github.com/kyverno/policy-reporter/pkg/database" + "github.com/kyverno/policy-reporter/pkg/email/violations" + "github.com/kyverno/policy-reporter/pkg/helper" + "github.com/kyverno/policy-reporter/pkg/target" +) + +var defaultOrder = []string{"resource_namespace", "resource_name", "resource_uid", "policy", "rule", "message"} + +type APIHandler struct { + store *db.Store + targets *target.Collection + reporter *violations.Reporter +} + +func (h *APIHandler) Register(engine *gin.RouterGroup) error { + engine.GET("targets", h.ListTargets) + engine.GET("namespaces", h.ListNamespaces) + engine.GET("policy-reports", h.ListPolicyReports) + engine.GET("cluster-policy-reports", h.ListClusterPolicyReports) + engine.GET("rule-status-count", h.RuleStatusCounts) + engine.GET("html-report/violations", h.HTMLViolationsReport) + + ns := engine.Group("namespaced-resources") + ns.GET("sources", h.ListNamespacedFilter("source")) + ns.GET("categories", h.ListNamespacedFilter("category")) + ns.GET("policies", h.ListNamespacedFilter("policy")) + ns.GET("kinds", h.ListNamespacedFilter("resource_kind")) + ns.GET("resources", h.ListNamespacedResources) + ns.GET("status-counts", h.ListNamespacedStatusCounts) + ns.GET("results", h.ListNamespacedResults) + + cluster := engine.Group("cluster-resources") + cluster.GET("sources", h.ListClusterFilter("source")) + cluster.GET("categories", h.ListClusterFilter("category")) + cluster.GET("policies", h.ListClusterFilter("policy")) + cluster.GET("kinds", h.ListClusterFilter("resource_kind")) + cluster.GET("resources", h.ListClusterResources) + cluster.GET("status-counts", h.ListClusterStatusCounts) + cluster.GET("results", h.ListClusterResults) + + return nil +} + +func (h *APIHandler) ListTargets(ctx *gin.Context) { + ctx.JSON(http.StatusOK, helper.Map(h.targets.Clients(), mapTarget)) +} + +func (h *APIHandler) ListPolicyReports(ctx *gin.Context) { + filter := api.BuildFilter(ctx) + + count, err := h.store.CountPolicyReports(ctx, filter) + list, err := h.store.FetchPolicyReports(ctx, filter, api.BuildPagination(ctx, []string{"namespace", "name"})) + + api.SendResponse(ctx, api.Paginated[PolicyReport]{Count: count, Items: MapPolicyReports(list)}, "failed to load policy reports", err) +} + +func (h *APIHandler) ListClusterPolicyReports(ctx *gin.Context) { + filter := api.BuildFilter(ctx) + + count, err := h.store.CountClusterPolicyReports(ctx, filter) + list, err := h.store.FetchClusterPolicyReports(ctx, filter, api.BuildPagination(ctx, []string{"name"})) + + api.SendResponse(ctx, api.Paginated[PolicyReport]{Count: count, Items: MapPolicyReports(list)}, "failed to load policy reports", err) +} + +func (h *APIHandler) ListNamespaces(ctx *gin.Context) { + list, err := h.store.FetchNamespaces(ctx, api.BuildFilter(ctx)) + + api.SendResponse(ctx, list, "failed to load namespaces", err) +} + +func (h *APIHandler) RuleStatusCounts(ctx *gin.Context) { + list, err := h.store.FetchRuleStatusCounts(ctx, ctx.Query("policy"), ctx.Query("rule")) + + api.SendResponse(ctx, MapRuleStatusCounts(list), "failed to load namespaces", err) +} + +func (h *APIHandler) ListClusterFilter(filter string) gin.HandlerFunc { + return func(ctx *gin.Context) { + list, err := h.store.FetchClusterFilter(ctx, filter, api.BuildFilter(ctx)) + + api.SendResponse(ctx, list, fmt.Sprintf("failed to load cluster scoped %s list", filter), err) + } +} + +func (h *APIHandler) ListNamespacedFilter(filter string) gin.HandlerFunc { + return func(ctx *gin.Context) { + list, err := h.store.FetchNamespacedFilter(ctx, filter, api.BuildFilter(ctx)) + + api.SendResponse(ctx, list, fmt.Sprintf("failed to load namespace scoped %s list", filter), err) + } +} + +func (h *APIHandler) ListClusterResources(ctx *gin.Context) { + list, err := h.store.FetchClusterResources(ctx, api.BuildFilter(ctx)) + + api.SendResponse(ctx, MapResource(list), "failed to load cluster scoped resource list", err) +} + +func (h *APIHandler) ListNamespacedResources(ctx *gin.Context) { + list, err := h.store.FetchNamespacedResources(ctx, api.BuildFilter(ctx)) + + api.SendResponse(ctx, MapResource(list), "failed to load namespace scoped resource list", err) +} + +func (h *APIHandler) ListClusterStatusCounts(ctx *gin.Context) { + filter := api.BuildFilter(ctx) + list, err := h.store.FetchClusterScopedStatusCounts(ctx, filter) + + api.SendResponse(ctx, MapClusterStatusCounts(list, filter.Status), "failed to load cluster scoped status counts", err) +} + +func (h *APIHandler) ListNamespacedStatusCounts(ctx *gin.Context) { + filter := api.BuildFilter(ctx) + list, err := h.store.FetchNamespaceScopedStatusCounts(ctx, filter) + + api.SendResponse(ctx, MapNamespaceStatusCounts(list, filter.Status), "failed to load namespace scoped status counts", err) +} + +func (h *APIHandler) ListClusterResults(ctx *gin.Context) { + filter := api.BuildFilter(ctx) + + count, err := h.store.CountResults(ctx, false, filter) + list, err := h.store.FetchResults(ctx, false, filter, api.BuildPagination(ctx, defaultOrder)) + + api.SendResponse(ctx, api.Paginated[Result]{Count: count, Items: MapResults(list)}, "failed to load results", err) +} + +func (h *APIHandler) ListNamespacedResults(ctx *gin.Context) { + filter := api.BuildFilter(ctx) + + count, err := h.store.CountResults(ctx, true, filter) + list, err := h.store.FetchResults(ctx, true, filter, api.BuildPagination(ctx, defaultOrder)) + + api.SendResponse(ctx, api.Paginated[Result]{Count: count, Items: MapResults(list)}, "failed to load results", err) +} + +func (h *APIHandler) HTMLViolationsReport(ctx *gin.Context) { + sources := make([]violations.Source, 0) + + list, err := h.store.FetchSources(ctx, db.Filter{}) + if err != nil { + zap.L().Error("failed to load data", zap.Error(err)) + ctx.AbortWithStatus(http.StatusInternalServerError) + return + } + + for _, source := range list { + cPass, err := h.store.CountResults(ctx, true, db.Filter{ + Sources: []string{source}, + Status: []string{"pass"}, + }) + if err != nil { + continue + } + + statusCounts, err := h.store.FetchNamespaceStatusCounts(ctx, source, db.Filter{ + Sources: []string{source}, + Status: []string{"pass"}, + }) + if err != nil { + continue + } + + nsPass := make(map[string]int, len(statusCounts)) + for _, s := range statusCounts { + nsPass[s.Namespace] = s.Count + } + + clusterResults, err := h.store.FetchResults(ctx, true, db.Filter{ + Sources: []string{source}, + Status: []string{"warn", "fail", "error"}, + }, db.Pagination{SortBy: defaultOrder}) + if err != nil { + continue + } + + cResults := make(map[string][]violations.Result) + for _, r := range clusterResults { + if _, ok := cResults[r.Result]; !ok { + cResults[r.Result] = make([]violations.Result, 0) + } + + cResults[r.Result] = append(cResults[r.Result], violations.Result{ + Kind: r.Resource.Kind, + Name: r.Resource.Name, + Policy: r.Policy, + Rule: r.Rule, + Status: r.Result, + }) + } + + namespaces, err := h.store.FetchNamespaces(ctx, db.Filter{ + Sources: []string{source}, + }) + if err != nil { + continue + } + + nsResults := make(map[string]map[string][]violations.Result) + for _, ns := range namespaces { + results, err := h.store.FetchResults(ctx, true, db.Filter{ + Sources: []string{source}, + Status: []string{"warn", "fail", "error"}, + Namespaces: []string{ns}, + }, db.Pagination{SortBy: defaultOrder}) + if err != nil { + continue + } + + mapping := make(map[string][]violations.Result) + mapping["warn"] = make([]violations.Result, 0) + mapping["fail"] = make([]violations.Result, 0) + mapping["error"] = make([]violations.Result, 0) + + for _, r := range results { + mapping[r.Result] = append(mapping[r.Result], violations.Result{ + Kind: r.Resource.Kind, + Name: r.Resource.Name, + Policy: r.Policy, + Rule: r.Rule, + Status: r.Result, + }) + } + + nsResults[ns] = mapping + } + + sources = append(sources, violations.Source{ + Name: source, + ClusterReports: (cPass + len(cResults)) > 0, + ClusterPassed: cPass, + ClusterResults: cResults, + NamespacePassed: nsPass, + NamespaceResults: nsResults, + }) + } + + data, err := h.reporter.Report(sources, "HTML") + if err != nil { + zap.L().Error("failed to load data", zap.Error(err)) + ctx.AbortWithStatus(http.StatusInternalServerError) + return + } + + ctx.Data(http.StatusOK, "text/html; charset=utf-8", []byte(data.Message)) +} + +func NewAPIHandler(store *db.Store, targets *target.Collection, reporter *violations.Reporter) *APIHandler { + return &APIHandler{store, targets, reporter} +} + +func WithAPI(store *db.Store, targets *target.Collection, reporter *violations.Reporter) api.ServerOption { + return func(s *api.Server) error { + return s.Register("v1", NewAPIHandler(store, targets, reporter)) + } +} diff --git a/pkg/api/v1/api_test.go b/pkg/api/v1/api_test.go new file mode 100644 index 00000000..c9bb8897 --- /dev/null +++ b/pkg/api/v1/api_test.go @@ -0,0 +1,301 @@ +package v1_test + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + + "github.com/kyverno/policy-reporter/pkg/api" + v1 "github.com/kyverno/policy-reporter/pkg/api/v1" + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" + "github.com/kyverno/policy-reporter/pkg/database" + "github.com/kyverno/policy-reporter/pkg/email/violations" + "github.com/kyverno/policy-reporter/pkg/fixtures" + "github.com/kyverno/policy-reporter/pkg/report" + "github.com/kyverno/policy-reporter/pkg/report/result" + "github.com/kyverno/policy-reporter/pkg/target" + "github.com/kyverno/policy-reporter/pkg/target/webhook" +) + +var reconditioner = result.NewReconditioner(nil) + +func TestV1(t *testing.T) { + db, err := database.NewSQLiteDB("db_v2.db") + if err != nil { + assert.Fail(t, "failed to init SQLite DB") + } + + store, err := database.NewStore(db, "1.0") + if err != nil { + assert.Fail(t, "failed to init Store") + } + + if err := store.PrepareDatabase(context.Background()); err != nil { + assert.Fail(t, "failed to prepare Store") + } + + store.Add(context.Background(), reconditioner.Prepare(fixtures.DefaultPolicyReport)) + store.Add(context.Background(), reconditioner.Prepare(fixtures.KyvernoPolicyReport)) + store.Add(context.Background(), reconditioner.Prepare(fixtures.KyvernoClusterPolicyReport)) + + gin.SetMode(gin.ReleaseMode) + + server := api.NewServer(gin.New(), v1.WithAPI(store, target.NewCollection(&target.Target{ + Client: webhook.NewClient(webhook.Options{ + ClientOptions: target.ClientOptions{ + Name: "Webhook", + SkipExistingOnStartup: true, + ResultFilter: &report.ResultFilter{ + MinimumSeverity: "", + Sources: []string{"Kyverno"}, + }, + }, + Host: "http://localhost:8080", + }), + }), violations.NewReporter("../../../templates", "Cluster", "Report"))) + + t.Run("TargetResponse", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v1/targets", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := make([]v1.Target, 0) + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, 1, len(resp)) + assert.Contains(t, resp, v1.Target{ + Name: "Webhook", + MinimumSeverity: v1alpha2.SeverityInfo, + Sources: []string{"Kyverno"}, + SkipExistingOnStartup: true, + }) + } + }) + + t.Run("ListPolicyReports", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v1/policy-reports", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := api.Paginated[v1.PolicyReport]{} + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, 2, resp.Count) + } + }) + + t.Run("ListClusterPolicyReports", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v1/cluster-policy-reports", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := api.Paginated[v1.PolicyReport]{} + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, 1, resp.Count) + } + }) + + t.Run("ListNamespaces", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v1/namespaces", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := make([]string, 0) + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, 2, len(resp)) + } + }) + + t.Run("RuleStatusCounts", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v1/rule-status-count?policy=required-limit&rule=resource-limit-required", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := make([]v1.StatusCount, 0) + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, 5, len(resp)) + assert.Contains(t, resp, v1.StatusCount{Status: "pass", Count: 1}) + assert.Contains(t, resp, v1.StatusCount{Status: "warn", Count: 1}) + } + }) + + t.Run("ListClusterFilter(Source)", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v1/cluster-resources/sources", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := make([]string, 0) + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, 1, len(resp)) + assert.Contains(t, resp, "Kyverno") + } + }) + + t.Run("ListNamespacedFilter(Source)", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v1/namespaced-resources/sources", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := make([]string, 0) + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, 2, len(resp)) + assert.Contains(t, resp, "Kyverno") + } + }) + + t.Run("ListClusterResources", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v1/cluster-resources/resources", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := make([]v1.Resource, 0) + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, 1, len(resp)) + } + }) + + t.Run("ListNamespacedResources", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v1/namespaced-resources/resources", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := make([]v1.Resource, 0) + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, 4, len(resp)) + } + }) + + t.Run("ListClusterStatusCounts", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v1/cluster-resources/status-counts", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := make([]v1.StatusCount, 0) + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, 5, len(resp)) + } + }) + + t.Run("ListNamespacedStatusCounts", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v1/namespaced-resources/status-counts", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := make([]v1.NamespaceCount, 0) + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, 5, len(resp)) + } + }) + + t.Run("ListClusterResults", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v1/cluster-resources/results", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := api.Paginated[v1.Result]{} + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, 1, resp.Count) + } + }) + + t.Run("ListNamespacedResults", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v1/namespaced-resources/results", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := api.Paginated[v1.Result]{} + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, 5, resp.Count) + } + }) + + t.Run("HTMLViolationsReport", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v1/html-report/violations", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + }) +} diff --git a/pkg/api/v1/finder.go b/pkg/api/v1/finder.go deleted file mode 100644 index 872cc741..00000000 --- a/pkg/api/v1/finder.go +++ /dev/null @@ -1,81 +0,0 @@ -package v1 - -import ( - "context" -) - -type Filter struct { - Kinds []string - Categories []string - Namespaces []string - Sources []string - Policies []string - Rules []string - Severities []string - Status []string - Resources []string - ReportLabel map[string]string - Search string -} - -type Pagination struct { - Page int - Offset int - SortBy []string - Direction string -} - -type PolicyReportFinder interface { - // FetchClusterPolicyReports by filter and pagination - FetchClusterPolicyReports(context.Context, Filter, Pagination) ([]*PolicyReport, error) - // FetchPolicyReports by filter and pagination - FetchPolicyReports(context.Context, Filter, Pagination) ([]*PolicyReport, error) - // CountClusterPolicyReports by filter - CountClusterPolicyReports(context.Context, Filter) (int, error) - // CountPolicyReports by filter - CountPolicyReports(context.Context, Filter) (int, error) - // FetchClusterPolicies from current PolicyReportResults - FetchClusterPolicies(context.Context, Filter) ([]string, error) - // FetchClusterRules from current PolicyReportResults - FetchClusterRules(context.Context, Filter) ([]string, error) - // FetchNamespacedPolicies from current PolicyReportResults with a Namespace - FetchNamespacedPolicies(context.Context, Filter) ([]string, error) - // FetchNamespacedRules from current PolicyReportResults with a Namespace - FetchNamespacedRules(context.Context, Filter) ([]string, error) - // FetchClusterCategories from current PolicyReportResults - FetchClusterCategories(context.Context, Filter) ([]string, error) - // FetchNamespacedCategories from current PolicyReportResults - FetchNamespacedCategories(context.Context, Filter) ([]string, error) - // FetchClusterSources from current PolicyReportResults - FetchClusterSources(context.Context) ([]string, error) - // FetchNamespacedSources from current PolicyReportResults with a Namespace - FetchNamespacedSources(context.Context) ([]string, error) - // FetchNamespacedKinds from current PolicyReportResults with a Namespace - FetchNamespacedKinds(context.Context, Filter) ([]string, error) - // FetchNamespacedResources from current PolicyReportResults with a Namespace - FetchNamespacedResources(context.Context, Filter) ([]*Resource, error) - // FetchClusterResources from current PolicyReportResults - FetchClusterResources(context.Context, Filter) ([]*Resource, error) - // FetchClusterKinds from current PolicyReportResults - FetchClusterKinds(context.Context, Filter) ([]string, error) - // FetchNamespaces from current PolicyReports - FetchNamespaces(context.Context, Filter) ([]string, error) - // FetchNamespacedStatusCounts from current PolicyReportResults with a Namespace - FetchNamespacedStatusCounts(context.Context, Filter) ([]NamespacedStatusCount, error) - // FetchClusterStatusCounts from current PolicyReportResults - FetchClusterStatusCounts(context.Context, Filter) ([]StatusCount, error) - // FetchNamespacedResults from current PolicyReportResults with a Namespace - FetchNamespacedResults(context.Context, Filter, Pagination) ([]*ListResult, error) - // FetchClusterResults from current PolicyReportResults - FetchClusterResults(context.Context, Filter, Pagination) ([]*ListResult, error) - // CountNamespacedResults from current PolicyReportResults with a Namespace - CountNamespacedResults(context.Context, Filter) (int, error) - // CountClusterResults from current PolicyReportResults - CountClusterResults(context.Context, Filter) (int, error) - // FetchRuleStatusCounts from current PolicyReportResults - FetchRuleStatusCounts(context.Context, string, string) ([]StatusCount, error) - // FetchClusterReportLabels from ClusterPolicyReports - FetchClusterReportLabels(context.Context, Filter) (map[string][]string, error) - // FetchNamespacedReportLabels from PolicyReports - FetchNamespacedReportLabels(context.Context, Filter) (map[string][]string, error) -} diff --git a/pkg/api/v1/handler.go b/pkg/api/v1/handler.go deleted file mode 100644 index 93935bbe..00000000 --- a/pkg/api/v1/handler.go +++ /dev/null @@ -1,310 +0,0 @@ -package v1 - -import ( - "net/http" - "strconv" - "strings" - - "go.uber.org/zap" - - "github.com/kyverno/policy-reporter/pkg/helper" - "github.com/kyverno/policy-reporter/pkg/target" -) - -var defaultOrder = []string{"resource_namespace", "resource_name", "resource_uid", "policy", "rule", "message"} - -type Handler struct { - finder PolicyReportFinder -} - -func (h *Handler) logError(err error) { - if err != nil { - zap.L().Error("failed to load data", zap.Error(err)) - } -} - -// TargetsHandler for the Targets REST API -func (h *Handler) TargetsHandler(targets []target.Client) http.HandlerFunc { - apiTargets := make([]Target, 0, len(targets)) - for _, t := range targets { - apiTargets = append(apiTargets, mapTarget(t)) - } - - return func(w http.ResponseWriter, req *http.Request) { - helper.SendJSONResponse(w, apiTargets, nil) - } -} - -// PolicyReportListHandler REST API -func (h *Handler) PolicyReportListHandler() http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - filter := buildFilter(req) - count, _ := h.finder.CountPolicyReports(req.Context(), filter) - list, err := h.finder.FetchPolicyReports(req.Context(), filter, buildPagination(req, []string{"namespace", "name"})) - h.logError(err) - helper.SendJSONResponse(w, PolicyReportList{Items: list, Count: count}, err) - } -} - -// PolicyReportListHandler REST API -func (h *Handler) ClusterPolicyReportListHandler() http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - filter := buildFilter(req) - count, _ := h.finder.CountClusterPolicyReports(req.Context(), filter) - list, err := h.finder.FetchClusterPolicyReports(req.Context(), filter, buildPagination(req, []string{"namespace", "name"})) - h.logError(err) - helper.SendJSONResponse(w, PolicyReportList{Items: list, Count: count}, err) - } -} - -// ClusterResourcesPolicyListHandler REST API -func (h *Handler) ClusterResourcesPolicyListHandler() http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - list, err := h.finder.FetchClusterPolicies(req.Context(), buildFilter(req)) - h.logError(err) - helper.SendJSONResponse(w, list, err) - } -} - -// ClusterResourcesRuleListHandler REST API -func (h *Handler) ClusterResourcesRuleListHandler() http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - list, err := h.finder.FetchClusterRules(req.Context(), buildFilter(req)) - h.logError(err) - helper.SendJSONResponse(w, list, err) - } -} - -// NamespacedResourcesPolicyListHandler REST API -func (h *Handler) NamespacedResourcesPolicyListHandler() http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - list, err := h.finder.FetchNamespacedPolicies(req.Context(), buildFilter(req)) - h.logError(err) - helper.SendJSONResponse(w, list, err) - } -} - -// NamespacedResourcesRuleListHandler REST API -func (h *Handler) NamespacedResourcesRuleListHandler() http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - list, err := h.finder.FetchNamespacedRules(req.Context(), buildFilter(req)) - h.logError(err) - helper.SendJSONResponse(w, list, err) - } -} - -// CategoryListHandler REST API -func (h *Handler) ClusterCategoryListHandler() http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - list, err := h.finder.FetchClusterCategories(req.Context(), buildFilter(req)) - h.logError(err) - helper.SendJSONResponse(w, list, err) - } -} - -// CategoryListHandler REST API -func (h *Handler) NamespacedCategoryListHandler() http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - list, err := h.finder.FetchNamespacedCategories(req.Context(), buildFilter(req)) - h.logError(err) - helper.SendJSONResponse(w, list, err) - } -} - -// ClusterResourcesKindListHandler REST API -func (h *Handler) ClusterResourcesKindListHandler() http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - list, err := h.finder.FetchClusterKinds(req.Context(), buildFilter(req)) - h.logError(err) - helper.SendJSONResponse(w, list, err) - } -} - -// NamespacedResourcesKindListHandler REST API -func (h *Handler) NamespacedResourcesKindListHandler() http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - list, err := h.finder.FetchNamespacedKinds(req.Context(), buildFilter(req)) - h.logError(err) - helper.SendJSONResponse(w, list, err) - } -} - -// ClusterResourcesListHandler REST API -func (h *Handler) ClusterResourcesListHandler() http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - list, err := h.finder.FetchClusterResources(req.Context(), buildFilter(req)) - h.logError(err) - helper.SendJSONResponse(w, list, err) - } -} - -// NamespacedResourcesListHandler REST API -func (h *Handler) NamespacedResourcesListHandler() http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - list, err := h.finder.FetchNamespacedResources(req.Context(), buildFilter(req)) - h.logError(err) - helper.SendJSONResponse(w, list, err) - } -} - -// ClusterResourcesSourceListHandler REST API -func (h *Handler) ClusterResourcesSourceListHandler() http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - list, err := h.finder.FetchClusterSources(req.Context()) - h.logError(err) - helper.SendJSONResponse(w, list, err) - } -} - -// NamespacedSourceListHandler REST API -func (h *Handler) NamespacedSourceListHandler() http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - list, err := h.finder.FetchNamespacedSources(req.Context()) - h.logError(err) - helper.SendJSONResponse(w, list, err) - } -} - -// NamespacedReportLabelListHandler REST API -func (h *Handler) NamespacedReportLabelListHandler() http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - list, err := h.finder.FetchNamespacedReportLabels(req.Context(), buildFilter(req)) - h.logError(err) - helper.SendJSONResponse(w, list, err) - } -} - -// ClusterReportLabelListHandler REST API -func (h *Handler) ClusterReportLabelListHandler() http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - list, err := h.finder.FetchClusterReportLabels(req.Context(), buildFilter(req)) - h.logError(err) - helper.SendJSONResponse(w, list, err) - } -} - -// ClusterResourcesStatusCountHandler REST API -func (h *Handler) ClusterResourcesStatusCountHandler() http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - list, err := h.finder.FetchClusterStatusCounts(req.Context(), buildFilter(req)) - h.logError(err) - helper.SendJSONResponse(w, list, err) - } -} - -// NamespacedResourcesStatusCountsHandler REST API -func (h *Handler) NamespacedResourcesStatusCountsHandler() http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - list, err := h.finder.FetchNamespacedStatusCounts(req.Context(), buildFilter(req)) - h.logError(err) - helper.SendJSONResponse(w, list, err) - } -} - -// RuleStatusCountHandler REST API -func (h *Handler) RuleStatusCountHandler() http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - list, err := h.finder.FetchRuleStatusCounts( - req.Context(), - req.URL.Query().Get("policy"), - req.URL.Query().Get("rule"), - ) - h.logError(err) - helper.SendJSONResponse(w, list, err) - } -} - -// NamespacedResourcesResultHandler REST API -func (h *Handler) NamespacedResourcesResultHandler() http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - filter := buildFilter(req) - count, _ := h.finder.CountNamespacedResults(req.Context(), filter) - list, err := h.finder.FetchNamespacedResults(req.Context(), filter, buildPagination(req, defaultOrder)) - h.logError(err) - helper.SendJSONResponse(w, ResultList{Items: list, Count: count}, err) - } -} - -// ClusterResourcesResultHandler REST API -func (h *Handler) ClusterResourcesResultHandler() http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - filter := buildFilter(req) - count, _ := h.finder.CountClusterResults(req.Context(), filter) - list, err := h.finder.FetchClusterResults(req.Context(), filter, buildPagination(req, defaultOrder)) - h.logError(err) - helper.SendJSONResponse(w, ResultList{Items: list, Count: count}, err) - } -} - -// NamespaceListHandler REST API -func (h *Handler) NamespaceListHandler() http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - list, err := h.finder.FetchNamespaces(req.Context(), Filter{ - Sources: req.URL.Query()["sources"], - Categories: req.URL.Query()["categories"], - Policies: req.URL.Query()["policies"], - Rules: req.URL.Query()["rules"], - }) - h.logError(err) - helper.SendJSONResponse(w, list, err) - } -} - -func buildPagination(req *http.Request, defaultOrder []string) Pagination { - page, err := strconv.Atoi(req.URL.Query().Get("page")) - if err != nil || page < 1 { - page = 0 - } - offset, err := strconv.Atoi(req.URL.Query().Get("offset")) - if err != nil || offset < 1 { - offset = 0 - } - direction := "ASC" - if strings.ToLower(req.URL.Query().Get("direction")) == "desc" { - direction = "DESC" - } - sortBy := req.URL.Query()["sortBy"] - if len(sortBy) == 0 { - sortBy = defaultOrder - } - - return Pagination{ - Page: page, - Offset: offset, - SortBy: sortBy, - Direction: direction, - } -} - -func buildFilter(req *http.Request) Filter { - labels := map[string]string{} - - for _, label := range req.URL.Query()["labels"] { - parts := strings.Split(label, ":") - if len(parts) != 2 { - continue - } - - labels[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) - } - - return Filter{ - Namespaces: req.URL.Query()["namespaces"], - Kinds: req.URL.Query()["kinds"], - Resources: req.URL.Query()["resources"], - Sources: req.URL.Query()["sources"], - Categories: req.URL.Query()["categories"], - Severities: req.URL.Query()["severities"], - Policies: req.URL.Query()["policies"], - Rules: req.URL.Query()["rules"], - Status: req.URL.Query()["status"], - ReportLabel: labels, - Search: req.URL.Query().Get("search"), - } -} - -func NewHandler(finder PolicyReportFinder) *Handler { - return &Handler{ - finder: finder, - } -} diff --git a/pkg/api/v1/handler_test.go b/pkg/api/v1/handler_test.go deleted file mode 100644 index ae286c4f..00000000 --- a/pkg/api/v1/handler_test.go +++ /dev/null @@ -1,667 +0,0 @@ -package v1_test - -import ( - "context" - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" - - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - v1 "github.com/kyverno/policy-reporter/pkg/api/v1" - "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" - "github.com/kyverno/policy-reporter/pkg/database" - "github.com/kyverno/policy-reporter/pkg/email/violations" - "github.com/kyverno/policy-reporter/pkg/target" - "github.com/kyverno/policy-reporter/pkg/target/loki" -) - -var seconds = time.Date(2022, 9, 6, 0, 0, 0, 0, time.UTC).Unix() - -var result1 = v1alpha2.PolicyReportResult{ - ID: "123", - Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/", - Policy: "require-requests-and-limits-required", - Rule: "autogen-check-for-requests-and-limits", - Priority: v1alpha2.ErrorPriority, - Result: v1alpha2.StatusFail, - Category: "Best Practices", - Severity: v1alpha2.SeverityHigh, - Scored: true, - Source: "Kyverno", - Timestamp: metav1.Timestamp{Seconds: seconds}, - Resources: []corev1.ObjectReference{{ - APIVersion: "v1", - Kind: "Deployment", - Name: "nginx", - Namespace: "test", - UID: "536ab69f-1b3c-4bd9-9ba4-274a56188409", - }}, -} - -var result2 = v1alpha2.PolicyReportResult{ - ID: "124", - Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/", - Policy: "require-requests-and-limits-required", - Rule: "autogen-check-for-requests-and-limits", - Priority: v1alpha2.WarningPriority, - Result: v1alpha2.StatusPass, - Category: "Best Practices", - Scored: true, - Source: "Kyverno", - Timestamp: metav1.Timestamp{Seconds: seconds}, - Resources: []corev1.ObjectReference{{ - APIVersion: "v1", - Kind: "Pod", - Name: "nginx", - Namespace: "test", - UID: "536ab69f-1b3c-4bd9-9ba4-274a56188419", - }}, -} - -var cresult1 = v1alpha2.PolicyReportResult{ - ID: "125", - Message: "validation error: The label `test` is required. Rule check-for-labels-on-namespace", - Policy: "require-ns-labels", - Rule: "check-for-labels-on-namespace", - Priority: v1alpha2.ErrorPriority, - Result: v1alpha2.StatusPass, - Category: "Convention", - Severity: v1alpha2.SeverityMedium, - Scored: true, - Source: "Kyverno", - Timestamp: metav1.Timestamp{Seconds: seconds}, - Resources: []corev1.ObjectReference{{ - APIVersion: "v1", - Kind: "Namespace", - Name: "test", - UID: "536ab69f-1b3c-4bd9-9ba4-274a56188411", - }}, -} - -var cresult2 = v1alpha2.PolicyReportResult{ - ID: "126", - Message: "validation error: The label `test` is required. Rule check-for-labels-on-namespace", - Policy: "require-ns-labels", - Rule: "check-for-labels-on-namespace", - Priority: v1alpha2.WarningPriority, - Result: v1alpha2.StatusFail, - Category: "Convention", - Severity: v1alpha2.SeverityHigh, - Scored: true, - Source: "Kyverno", - Resources: []corev1.ObjectReference{{ - APIVersion: "v1", - Kind: "Namespace", - Name: "dev", - UID: "536ab69f-1b3c-4bd9-9ba4-274a56188412", - }}, -} - -var preport = &v1alpha2.PolicyReport{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{"app": "policy-reporter", "scope": "namespace"}, - Name: "polr-test", - Namespace: "test", - CreationTimestamp: metav1.Now(), - }, - Results: []v1alpha2.PolicyReportResult{result1, result2}, - Summary: v1alpha2.PolicyReportSummary{Fail: 1}, -} - -var creport = &v1alpha2.ClusterPolicyReport{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{"app": "policy-reporter", "scope": "cluster"}, - Name: "cpolr", - CreationTimestamp: metav1.Now(), - }, - Results: []v1alpha2.PolicyReportResult{cresult1, cresult2}, - Summary: v1alpha2.PolicyReportSummary{}, -} - -var ctx = context.Background() - -func Test_V1_API(t *testing.T) { - db, err := database.NewSQLiteDB("test.db") - if err != nil { - t.Error(err) - } - defer db.Close() - if err != nil { - t.Fatal(err) - } - store, err := database.NewStore(db, "test") - if err != nil { - t.Fatal(err) - } - store.PrepareDatabase(ctx) - defer store.CleanUp(ctx) - - store.Add(ctx, preport) - store.Add(ctx, creport) - - handl := v1.NewHandler(store) - htmlHandl := v1.NewHTMLHandler(store, violations.NewReporter("../../../templates", "Cluster", "Report")) - - t.Run("ClusterPolicyListHandler", func(t *testing.T) { - req, err := http.NewRequest("GET", "/v1/cluster-policies", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler := handl.ClusterResourcesPolicyListHandler() - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - - expected := `["require-ns-labels"]` - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) - } - }) - - t.Run("ClusterRuleListHandler", func(t *testing.T) { - req, err := http.NewRequest("GET", "/v1/cluster-rules", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler := handl.ClusterResourcesRuleListHandler() - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - - expected := `["check-for-labels-on-namespace"]` - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) - } - }) - - t.Run("NamespacedPolicyListHandler", func(t *testing.T) { - req, err := http.NewRequest("GET", "/v1/namespaced-policies", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler := handl.NamespacedResourcesPolicyListHandler() - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - - expected := `["require-requests-and-limits-required"]` - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) - } - }) - - t.Run("NamespacedRuleListHandler", func(t *testing.T) { - req, err := http.NewRequest("GET", "/v1/namespaced-rules", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler := handl.NamespacedResourcesRuleListHandler() - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - - expected := `["autogen-check-for-requests-and-limits"]` - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) - } - }) - - t.Run("NamespacedCategoryListHandler", func(t *testing.T) { - req, err := http.NewRequest("GET", "/v1/categories", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler := handl.NamespacedCategoryListHandler() - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - - expected := `["Best Practices"]` - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) - } - }) - - t.Run("ClusterCategoryListHandler", func(t *testing.T) { - req, err := http.NewRequest("GET", "/v1/categories", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler := handl.ClusterCategoryListHandler() - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - - expected := `["Convention"]` - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) - } - }) - - t.Run("ClusterKindListHandler", func(t *testing.T) { - req, err := http.NewRequest("GET", "/v1/cluster-kinds", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler := handl.ClusterResourcesKindListHandler() - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - - expected := `["Namespace"]` - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) - } - }) - - t.Run("NamespacedKindListHandler", func(t *testing.T) { - req, err := http.NewRequest("GET", "/v1/namespaced-kinds", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler := handl.NamespacedResourcesKindListHandler() - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - - expected := `["Deployment","Pod"]` - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) - } - }) - - t.Run("ClusterResourcesListHandler", func(t *testing.T) { - req, err := http.NewRequest("GET", "/v1/cluster-resources/resources", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler := handl.ClusterResourcesListHandler() - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - - expected := `[{"name":"dev","kind":"Namespace"},{"name":"test","kind":"Namespace"}]` - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) - } - }) - - t.Run("NamespacedResourcesListHandler", func(t *testing.T) { - req, err := http.NewRequest("GET", "/v1/namespaced-resources/resources", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler := handl.NamespacedResourcesListHandler() - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - - expected := `[{"name":"nginx","kind":"Deployment"},{"name":"nginx","kind":"Pod"}]` - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) - } - }) - - t.Run("ClusterSourceListHandler", func(t *testing.T) { - req, err := http.NewRequest("GET", "/v1/cluster-sources", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler := handl.ClusterResourcesSourceListHandler() - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - - expected := `["Kyverno"]` - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) - } - }) - - t.Run("NamespacedSourceListHandler", func(t *testing.T) { - req, err := http.NewRequest("GET", "/v1/namspaced-sources", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler := handl.NamespacedSourceListHandler() - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - - expected := `["Kyverno"]` - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) - } - }) - - t.Run("ClusterStatusCountHandler", func(t *testing.T) { - req, err := http.NewRequest("GET", "/v1/cluster-status-counts?status=pass", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler := handl.ClusterResourcesStatusCountHandler() - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - - expected := `[{"status":"pass","count":1}]` - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) - } - }) - - t.Run("NamespacedStatusCountHandler", func(t *testing.T) { - req, err := http.NewRequest("GET", "/v1/namespaced-status-counts?status=pass", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler := handl.NamespacedResourcesStatusCountsHandler() - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - - expected := `[{"status":"pass","items":[{"namespace":"test","count":1}]}]` - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) - } - }) - - t.Run("RuleStatusCountHandler", func(t *testing.T) { - req, err := http.NewRequest("GET", "/v1/rule-status-count?policy=require-requests-and-limits-required&rule=autogen-check-for-requests-and-limits", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler := handl.RuleStatusCountHandler() - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - - expected := `{"status":"fail","count":1}` - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) - } - - expected = `{"status":"pass","count":1}` - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) - } - - expected = `{"status":"warn","count":0}` - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) - } - }) - - t.Run("NamespacedResultHandler", func(t *testing.T) { - req, err := http.NewRequest("GET", "/v1/namespaced-results?direction=desc", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler := handl.NamespacedResourcesResultHandler() - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - - expected := `[{"id":"123","namespace":"test","kind":"Deployment","apiVersion":"v1","name":"nginx","message":"validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/","category":"Best Practices","policy":"require-requests-and-limits-required","rule":"autogen-check-for-requests-and-limits","status":"fail","severity":"high","timestamp":1662422400},{"id":"124","namespace":"test","kind":"Pod","apiVersion":"v1","name":"nginx","message":"validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/","category":"Best Practices","policy":"require-requests-and-limits-required","rule":"autogen-check-for-requests-and-limits","status":"pass","timestamp":1662422400}]` - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) - } - }) - - t.Run("ClusterResultHandler", func(t *testing.T) { - req, err := http.NewRequest("GET", "/v1/cluster-results?direction=desc", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler := handl.ClusterResourcesResultHandler() - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - - expected := "{\"id\":\"125\",\"kind\":\"Namespace\",\"apiVersion\":\"v1\",\"name\":\"test\",\"message\":\"validation error: The label `test` is required. Rule check-for-labels-on-namespace\",\"category\":\"Convention\",\"policy\":\"require-ns-labels\",\"rule\":\"check-for-labels-on-namespace\",\"status\":\"pass\",\"severity\":\"medium\",\"timestamp\":1662422400}" - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) - } - }) - - t.Run("NamespaceListHandler", func(t *testing.T) { - req, err := http.NewRequest("GET", "/v1/namespaces", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler := handl.NamespaceListHandler() - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - - expected := `["test"]` - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) - } - }) - - t.Run("ClusterReportLabelListHandler", func(t *testing.T) { - req, err := http.NewRequest("GET", "/v1/cluster-resources/report-labels?sources=Kyverno", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler := handl.ClusterReportLabelListHandler() - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - - expected := `{"app":["policy-reporter"],"scope":["cluster"]}` - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) - } - }) - - t.Run("ClusterReportLabelListHandler", func(t *testing.T) { - req, err := http.NewRequest("GET", "/v1/namespaced-resources/report-labels?sources=Kyverno", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler := handl.NamespacedReportLabelListHandler() - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - - expected := `{"app":["policy-reporter"],"scope":["namespace"]}` - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) - } - }) - - t.Run("PolicyReportListHandler", func(t *testing.T) { - req, err := http.NewRequest("GET", "/v1/policy-reports?namespaces=test&labels=app:policy-reporter", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler := handl.PolicyReportListHandler() - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - - expected := `{"items":[{"id":"7605991845421273693","name":"polr-test","namespace":"test","source":"Kyverno","labels":{"app":"policy-reporter","scope":"namespace"},"pass":0,"skip":0,"warn":0,"error":0,"fail":1}],"count":1}` - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) - } - }) - - t.Run("ClusterPolicyReportListHandler", func(t *testing.T) { - req, err := http.NewRequest("GET", "/v1/policy-reports?labels=app:policy-reporter", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler := handl.ClusterPolicyReportListHandler() - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - - expected := `{"items":[{"id":"7174304213499286261","name":"cpolr","source":"Kyverno","labels":{"app":"policy-reporter","scope":"cluster"},"pass":0,"skip":0,"warn":0,"error":0,"fail":0}],"count":1}` - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) - } - }) - - t.Run("HTMLReport", func(t *testing.T) { - req, err := http.NewRequest("GET", "/v1/html-report/violations", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler := htmlHandl.HTMLReport() - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - }) -} - -func Test_TargetsAPI(t *testing.T) { - handl := v1.NewHandler(nil) - - t.Run("Empty Respose", func(t *testing.T) { - req, err := http.NewRequest("GET", "/targets", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler := handl.TargetsHandler(make([]target.Client, 0)) - - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - - expected := "[]" - - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) - } - }) - t.Run("Respose", func(t *testing.T) { - req, err := http.NewRequest("GET", "/targets", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler := handl.TargetsHandler([]target.Client{ - loki.NewClient(loki.Options{ - ClientOptions: target.ClientOptions{ - Name: "Loki", - SkipExistingOnStartup: true, - }, - HTTPClient: &http.Client{}, - }), - }) - - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - - expected := `[{"name":"Loki","minimumPriority":"debug","skipExistingOnStartup":true}]` - if !strings.Contains(rr.Body.String(), expected) { - t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) - } - }) -} diff --git a/pkg/api/v1/html_report.go b/pkg/api/v1/html_report.go deleted file mode 100644 index 550fefee..00000000 --- a/pkg/api/v1/html_report.go +++ /dev/null @@ -1,146 +0,0 @@ -package v1 - -import ( - "net/http" - "slices" - - "github.com/kyverno/policy-reporter/pkg/email/violations" - "go.uber.org/zap" -) - -type HTMLHandler struct { - reporter *violations.Reporter - finder PolicyReportFinder -} - -func (h *HTMLHandler) HTMLReport() http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - sources := make([]violations.Source, 0) - - namespaced, err := h.finder.FetchNamespacedSources(req.Context()) - if err != nil { - zap.L().Error("failed to load data", zap.Error(err)) - w.WriteHeader(http.StatusInternalServerError) - return - } - - cluster, err := h.finder.FetchClusterSources(req.Context()) - if err != nil { - zap.L().Error("failed to load data", zap.Error(err)) - w.WriteHeader(http.StatusInternalServerError) - return - } - - list := append(namespaced, cluster...) - slices.Sort(list) - list = slices.Compact(list) - - for _, source := range list { - cPass, err := h.finder.CountClusterResults(req.Context(), Filter{ - Sources: []string{source}, - Status: []string{"pass"}, - }) - if err != nil { - continue - } - - statusCounts, err := h.finder.FetchNamespacedStatusCounts(req.Context(), Filter{ - Sources: []string{source}, - Status: []string{"pass"}, - }) - if err != nil { - continue - } - - nsPass := make(map[string]int, len(statusCounts)) - for _, s := range statusCounts[0].Items { - nsPass[s.Namespace] = s.Count - } - - clusterResults, err := h.finder.FetchClusterResults(req.Context(), Filter{ - Sources: []string{source}, - Status: []string{"warn", "fail", "error"}, - }, Pagination{SortBy: defaultOrder}) - if err != nil { - continue - } - - cResults := make(map[string][]violations.Result) - for _, r := range clusterResults { - if _, ok := cResults[r.Status]; !ok { - cResults[r.Status] = make([]violations.Result, 0) - } - - cResults[r.Status] = append(cResults[r.Status], violations.Result{ - Kind: r.Kind, - Name: r.Name, - Policy: r.Policy, - Rule: r.Rule, - Status: r.Status, - }) - } - - namespaces, err := h.finder.FetchNamespaces(req.Context(), Filter{ - Sources: []string{source}, - }) - if err != nil { - continue - } - - nsResults := make(map[string]map[string][]violations.Result) - for _, ns := range namespaces { - results, err := h.finder.FetchNamespacedResults(req.Context(), Filter{ - Sources: []string{source}, - Status: []string{"warn", "fail", "error"}, - Namespaces: []string{ns}, - }, Pagination{SortBy: defaultOrder}) - if err != nil { - continue - } - - mapping := make(map[string][]violations.Result) - mapping["warn"] = make([]violations.Result, 0) - mapping["fail"] = make([]violations.Result, 0) - mapping["error"] = make([]violations.Result, 0) - - for _, r := range results { - mapping[r.Status] = append(mapping[r.Status], violations.Result{ - Kind: r.Kind, - Name: r.Name, - Policy: r.Policy, - Rule: r.Rule, - Status: r.Status, - }) - } - - nsResults[ns] = mapping - } - - sources = append(sources, violations.Source{ - Name: source, - ClusterReports: len(cluster) > 0, - ClusterPassed: cPass, - ClusterResults: cResults, - NamespacePassed: nsPass, - NamespaceResults: nsResults, - }) - } - - data, err := h.reporter.Report(sources, "HTML") - if err != nil { - zap.L().Error("failed to load data", zap.Error(err)) - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Write([]byte(data.Message)) - } -} - -func NewHTMLHandler(finder PolicyReportFinder, reporter *violations.Reporter) *HTMLHandler { - return &HTMLHandler{ - finder: finder, - reporter: reporter, - } -} diff --git a/pkg/api/v1/model.go b/pkg/api/v1/model.go index fc151a78..67280f52 100644 --- a/pkg/api/v1/model.go +++ b/pkg/api/v1/model.go @@ -2,9 +2,33 @@ package v1 import ( "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" + db "github.com/kyverno/policy-reporter/pkg/database" + "github.com/kyverno/policy-reporter/pkg/helper" "github.com/kyverno/policy-reporter/pkg/target" ) +// Target API Model +type Target struct { + Name string `json:"name"` + MinimumSeverity string `json:"minimumSeverity"` + Sources []string `json:"sources,omitempty"` + SkipExistingOnStartup bool `json:"skipExistingOnStartup"` +} + +func mapTarget(t target.Client) Target { + minSev := t.MinimumSeverity() + if minSev == "" { + minSev = v1alpha2.SeverityInfo + } + + return Target{ + Name: t.Name(), + MinimumSeverity: minSev, + Sources: t.Sources(), + SkipExistingOnStartup: t.SkipExistingOnStartup(), + } +} + type PolicyReport struct { ID string `json:"id"` Name string `json:"name"` @@ -16,28 +40,70 @@ type PolicyReport struct { Warn int `json:"warn"` Error int `json:"error"` Fail int `json:"fail"` - Type string `json:"-"` - Created int64 `json:"-"` } -type PolicyReportList struct { - Items []*PolicyReport `json:"items"` - Count int `json:"count"` -} - -type ResultList struct { - Items []*ListResult `json:"items"` - Count int `json:"count"` +func MapPolicyReports(results []db.PolicyReport) []PolicyReport { + return helper.Map(results, func(res db.PolicyReport) PolicyReport { + return PolicyReport{ + ID: res.ID, + Name: res.Name, + Namespace: res.Namespace, + Source: res.Source, + Labels: res.Labels, + Pass: res.Pass, + Fail: res.Fail, + Warn: res.Warn, + Error: res.Error, + Skip: res.Skip, + } + }) } type StatusCount struct { + Source string `json:"source,omitempty"` Status string `json:"status"` Count int `json:"count"` } -type NamespacedStatusCount struct { - Status string `json:"status"` - Items []NamespaceCount `json:"items"` +func MapRuleStatusCounts(results []db.StatusCount) []StatusCount { + mapping := map[string]StatusCount{ + v1alpha2.StatusPass: {Status: v1alpha2.StatusPass}, + v1alpha2.StatusFail: {Status: v1alpha2.StatusFail}, + v1alpha2.StatusWarn: {Status: v1alpha2.StatusWarn}, + v1alpha2.StatusError: {Status: v1alpha2.StatusError}, + v1alpha2.StatusSkip: {Status: v1alpha2.StatusSkip}, + } + + for _, result := range results { + mapping[result.Status] = StatusCount{Status: result.Status, Count: result.Count} + } + + return helper.ToList(mapping) +} + +func MapClusterStatusCounts(results []db.StatusCount, status []string) []StatusCount { + var mapping map[string]StatusCount + + if len(status) == 0 { + mapping = map[string]StatusCount{ + v1alpha2.StatusPass: {Status: v1alpha2.StatusPass}, + v1alpha2.StatusFail: {Status: v1alpha2.StatusFail}, + v1alpha2.StatusWarn: {Status: v1alpha2.StatusWarn}, + v1alpha2.StatusError: {Status: v1alpha2.StatusError}, + v1alpha2.StatusSkip: {Status: v1alpha2.StatusSkip}, + } + } else { + mapping = map[string]StatusCount{} + + for _, status := range status { + mapping[status] = StatusCount{Status: status} + } + } + for _, result := range results { + mapping[result.Status] = StatusCount{Status: result.Status, Count: result.Count} + } + + return helper.ToList(mapping) } type NamespaceCount struct { @@ -46,17 +112,73 @@ type NamespaceCount struct { Status string `json:"-"` } -type Resource struct { - Name string `json:"name"` - Kind string `json:"kind"` +type NamespaceStatusCount struct { + Status string `json:"status"` + Items []NamespaceCount `json:"items"` } -type ListResult struct { +func MapNamespaceStatusCounts(results []db.StatusCount, status []string) []NamespaceStatusCount { + var mapping map[string][]NamespaceCount + + if len(status) == 0 { + mapping = map[string][]NamespaceCount{ + v1alpha2.StatusPass: make([]NamespaceCount, 0), + v1alpha2.StatusFail: make([]NamespaceCount, 0), + v1alpha2.StatusWarn: make([]NamespaceCount, 0), + v1alpha2.StatusError: make([]NamespaceCount, 0), + v1alpha2.StatusSkip: make([]NamespaceCount, 0), + } + } else { + mapping = map[string][]NamespaceCount{} + + for _, status := range status { + mapping[status] = make([]NamespaceCount, 0) + } + } + for _, result := range results { + mapping[result.Status] = append(mapping[result.Status], NamespaceCount{Status: result.Status, Count: result.Count, Namespace: result.Namespace}) + } + + statusCounts := make([]NamespaceStatusCount, 0, 5) + for status, items := range mapping { + statusCounts = append(statusCounts, NamespaceStatusCount{ + Status: status, + Items: items, + }) + } + + return statusCounts +} + +type Resource struct { + ID string `json:"id,omitempty"` + UID string `json:"uid,omitempty"` + Name string `json:"name,omitempty"` + Namespace string `json:"namespace,omitempty"` + Kind string `json:"kind,omitempty"` + APIVersion string `json:"apiVersion,omitempty"` +} + +func MapResource(results []db.ResourceResult) []Resource { + return helper.Map(results, func(res db.ResourceResult) Resource { + return Resource{ + ID: res.ID, + UID: res.Resource.UID, + Name: res.Resource.Name, + Namespace: res.Resource.Namespace, + Kind: res.Resource.Kind, + APIVersion: res.Resource.APIVersion, + } + }) +} + +type Result struct { ID string `json:"id"` Namespace string `json:"namespace,omitempty"` Kind string `json:"kind"` APIVersion string `json:"apiVersion"` Name string `json:"name"` + ResourceID string `json:"resourceId"` Message string `json:"message"` Category string `json:"category,omitempty"` Policy string `json:"policy"` @@ -67,24 +189,23 @@ type ListResult struct { Properties map[string]string `json:"properties,omitempty"` } -// Target API Model -type Target struct { - Name string `json:"name"` - MinimumPriority string `json:"minimumPriority"` - Sources []string `json:"sources,omitempty"` - SkipExistingOnStartup bool `json:"skipExistingOnStartup"` -} - -func mapTarget(t target.Client) Target { - minPrio := t.MinimumPriority() - if minPrio == "" { - minPrio = v1alpha2.DebugPriority.String() - } - - return Target{ - Name: t.Name(), - MinimumPriority: minPrio, - Sources: t.Sources(), - SkipExistingOnStartup: t.SkipExistingOnStartup(), - } +func MapResults(results []db.PolicyReportResult) []Result { + return helper.Map(results, func(res db.PolicyReportResult) Result { + return Result{ + ID: res.ID, + Namespace: res.Resource.Namespace, + Kind: res.Resource.Kind, + APIVersion: res.Resource.APIVersion, + Name: res.Resource.Name, + ResourceID: res.Resource.GetID(), + Message: res.Message, + Category: res.Category, + Policy: res.Policy, + Rule: res.Rule, + Status: res.Result, + Severity: res.Severity, + Timestamp: res.Created, + Properties: res.Properties, + } + }) } diff --git a/pkg/api/v1/model_test.go b/pkg/api/v1/model_test.go new file mode 100644 index 00000000..c68254a3 --- /dev/null +++ b/pkg/api/v1/model_test.go @@ -0,0 +1,44 @@ +package v1_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + v1 "github.com/kyverno/policy-reporter/pkg/api/v1" + "github.com/kyverno/policy-reporter/pkg/database" +) + +func TestMapping(t *testing.T) { + t.Run("MapClusterStatusCounts", func(t *testing.T) { + result := v1.MapClusterStatusCounts([]database.StatusCount{ + {Source: "kyverno", Status: "pass", Count: 3}, + {Source: "kyverno", Status: "fail", Count: 4}, + }, []string{"pass", "fail"}) + + assert.Equal(t, 2, len(result)) + assert.Contains(t, result, v1.StatusCount{Status: "pass", Count: 3}) + assert.Contains(t, result, v1.StatusCount{Status: "fail", Count: 4}) + }) + + t.Run("MapNamespaceStatusCounts", func(t *testing.T) { + result := v1.MapNamespaceStatusCounts([]database.StatusCount{ + {Source: "kyverno", Status: "pass", Count: 3, Namespace: "default"}, + {Source: "kyverno", Status: "fail", Count: 4, Namespace: "default"}, + {Source: "kyverno", Status: "pass", Count: 2, Namespace: "user"}, + {Source: "kyverno", Status: "fail", Count: 2, Namespace: "user"}, + }, []string{"pass", "fail"}) + + assert.Equal(t, 2, len(result)) + + assert.Contains(t, result, v1.NamespaceStatusCount{Status: "pass", Items: []v1.NamespaceCount{ + {Namespace: "default", Count: 3, Status: "pass"}, + {Namespace: "user", Count: 2, Status: "pass"}, + }}) + + assert.Contains(t, result, v1.NamespaceStatusCount{Status: "fail", Items: []v1.NamespaceCount{ + {Namespace: "default", Count: 4, Status: "fail"}, + {Namespace: "user", Count: 2, Status: "fail"}, + }}) + }) +} diff --git a/pkg/api/v2/api.go b/pkg/api/v2/api.go new file mode 100644 index 00000000..c9315fd1 --- /dev/null +++ b/pkg/api/v2/api.go @@ -0,0 +1,286 @@ +package v2 + +import ( + "errors" + "net/http" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" + + "github.com/kyverno/policy-reporter/pkg/api" + db "github.com/kyverno/policy-reporter/pkg/database" + "github.com/kyverno/policy-reporter/pkg/kubernetes/namespaces" + "github.com/kyverno/policy-reporter/pkg/target" +) + +var defaultOrder = []string{"resource_namespace", "resource_name", "resource_uid", "policy", "rule", "message"} + +type APIHandler struct { + store *db.Store + nsClient namespaces.Client + targets map[string][]*Target +} + +func (h *APIHandler) Register(engine *gin.RouterGroup) error { + engine.GET("resource/:id/status-counts", h.GetResourceStatusCounts) + engine.GET("resource/:id/severity-counts", h.GetResourceSeverityCounts) + engine.GET("resource/:id/resource-results", h.ListResourceResults) + engine.GET("resource/:id/results", h.ListResourcePolilcyResults) + engine.GET("resource/:id", h.GetResource) + engine.GET("resource/:id/source-categories", h.ListResourceCategories) + + engine.POST("namespaces/resolve-selector", h.ResolveNamespaceSelector) + engine.GET("namespaces", h.ListNamespaces) + engine.GET("sources", h.ListSources) + engine.GET("sources/:source/use-resources", h.UseResources) + engine.GET("sources/:source/status-counts", h.GetTotalStatusCounts) + engine.GET("sources/:source/severity-counts", h.GetTotalSeverityCounts) + engine.GET("sources/categories", h.ListSourceWithCategories) + engine.GET("policies", h.ListPolicies) + engine.GET("findings", h.ListFindings) + engine.GET("severity-findings", h.ListSeverityFindings) + engine.GET("results-without-resources", h.ListResultsWithoutResource) + engine.GET("targets", h.ListTargets) + engine.GET("properties/:property", h.ListProperty) + + ns := engine.Group("namespace-scoped") + ns.GET("resource-results", h.ListNamespaceResourceResults) + ns.GET(":source/status-counts", h.GetNamespaceStatusCounts) + ns.GET(":source/severity-counts", h.GetNamespaceSeverityCounts) + ns.GET("kinds", h.ListNamespaceKinds) + ns.GET("results", h.ListPolicyResults(true)) + + cluster := engine.Group("cluster-scoped") + cluster.GET("resource-results", h.ListClusterResourceResults) + cluster.GET(":source/status-counts", h.GetClusterStatusCounts) + cluster.GET(":source/severity-counts", h.GetClusterSeverityCounts) + cluster.GET("kinds", h.ListClusterKinds) + cluster.GET("results", h.ListPolicyResults(false)) + + return nil +} + +func (h *APIHandler) ResolveNamespaceSelector(ctx *gin.Context) { + selector := make(map[string]string) + if err := ctx.BindJSON(&selector); err != nil { + zap.L().Error("resolve namespace selector: failed to convert request body", zap.Error(err)) + ctx.AbortWithError(http.StatusBadRequest, errors.New("invalid selector content")) + } + + list, err := h.nsClient.List(ctx, selector) + + api.SendResponse(ctx, list, "failed to get namespaces for the provided selector", err) +} + +func (h *APIHandler) ListNamespaces(ctx *gin.Context) { + list, err := h.store.FetchNamespaces(ctx, api.BuildFilter(ctx)) + + api.SendResponse(ctx, list, "failed to load namespaces", err) +} + +func (h *APIHandler) ListSources(ctx *gin.Context) { + sources, err := h.store.FetchSources(ctx, api.BuildFilter(ctx)) + + api.SendResponse(ctx, sources, "failed to load sources", err) +} + +func (h *APIHandler) ListPolicies(ctx *gin.Context) { + policies, err := h.store.FetchPolicies(ctx, api.BuildFilter(ctx)) + + api.SendResponse(ctx, MapPolicies(policies), "failed to load policies", err) +} + +func (h *APIHandler) ListSourceWithCategories(ctx *gin.Context) { + categories, err := h.store.FetchCategories(ctx, api.BuildFilter(ctx)) + + api.SendResponse(ctx, MapToSourceDetails(categories), "failed to load source details", err) +} + +func (h *APIHandler) ListResourceCategories(ctx *gin.Context) { + categories, err := h.store.FetchResourceCategories(ctx, ctx.Param("id"), api.BuildFilter(ctx)) + + api.SendResponse(ctx, MapResourceCategoryToSourceDetails(categories), "failed to load source details", err) +} + +func (h *APIHandler) GetResource(ctx *gin.Context) { + resource, err := h.store.FetchResource(ctx, ctx.Param("id")) + + api.SendResponse(ctx, MapResource(resource), "failed to load source details", err) +} + +func (h *APIHandler) ListProperty(ctx *gin.Context) { + list, err := h.store.FetchProperty(ctx, ctx.Param("property"), api.BuildFilter(ctx)) + + api.SendResponse(ctx, MapResultPropertyList(list), "failed to load property value list", err) +} + +func (h *APIHandler) GetResourceStatusCounts(ctx *gin.Context) { + counts, err := h.store.FetchResourceStatusCounts(ctx, ctx.Param("id"), api.BuildFilter(ctx)) + + api.SendResponse(ctx, MapResourceStatusCounts(counts), "failed to load resource status counts", err) +} + +func (h *APIHandler) GetResourceSeverityCounts(ctx *gin.Context) { + counts, err := h.store.FetchResourceSeverityCounts(ctx, ctx.Param("id"), api.BuildFilter(ctx)) + + api.SendResponse(ctx, MapResourceSeverityCounts(counts), "failed to load resource severity counts", err) +} + +func (h *APIHandler) ListNamespaceResourceResults(ctx *gin.Context) { + filter := api.BuildFilter(ctx) + list, err := h.store.FetchNamespaceResourceResults(ctx, filter, api.BuildPagination(ctx, []string{"resource_namespace", "resource_name", "resource_uid"})) + if err != nil { + zap.L().Error("failed to load resource results", zap.Error(err)) + ctx.AbortWithStatus(http.StatusInternalServerError) + return + } + count, err := h.store.CountNamespaceResourceResults(ctx, filter) + + api.SendResponse(ctx, Paginated[ResourceResult]{Count: count, Items: MapResourceResults(list)}, "failed to load resource result list", err) +} + +func (h *APIHandler) ListClusterResourceResults(ctx *gin.Context) { + filter := api.BuildFilter(ctx) + list, err := h.store.FetchClusterResourceResults(ctx, filter, api.BuildPagination(ctx, []string{"resource_namespace", "resource_name", "resource_uid"})) + if err != nil { + zap.L().Error("failed to load resource results", zap.Error(err)) + ctx.AbortWithStatus(http.StatusInternalServerError) + return + } + count, err := h.store.CountClusterResourceResults(ctx, filter) + + api.SendResponse(ctx, Paginated[ResourceResult]{Count: count, Items: MapResourceResults(list)}, "failed to load resource result list", err) +} + +func (h *APIHandler) GetClusterStatusCounts(ctx *gin.Context) { + results, err := h.store.FetchClusterStatusCounts(ctx, ctx.Param("source"), api.BuildFilter(ctx)) + + api.SendResponse(ctx, MapClusterStatusCounts(results), "failed to calculate cluster status counts", err) +} + +func (h *APIHandler) GetClusterSeverityCounts(ctx *gin.Context) { + results, err := h.store.FetchClusterSeverityCounts(ctx, ctx.Param("source"), api.BuildFilter(ctx)) + + api.SendResponse(ctx, MapClusterSeverityCounts(results), "failed to calculate cluster status counts", err) +} + +func (h *APIHandler) GetNamespaceStatusCounts(ctx *gin.Context) { + results, err := h.store.FetchNamespaceStatusCounts(ctx, ctx.Param("source"), api.BuildFilter(ctx)) + + api.SendResponse(ctx, MapNamespaceStatusCounts(results), "failed to calculate namespace status counts", err) +} + +func (h *APIHandler) GetNamespaceSeverityCounts(ctx *gin.Context) { + results, err := h.store.FetchNamespaceSeverityCounts(ctx, ctx.Param("source"), api.BuildFilter(ctx)) + + api.SendResponse(ctx, MapNamespaceSeverityCounts(results), "failed to calculate namespace severity counts", err) +} + +func (h *APIHandler) GetTotalStatusCounts(ctx *gin.Context) { + results, err := h.store.FetchTotalStatusCounts(ctx, ctx.Param("source"), api.BuildFilter(ctx)) + + api.SendResponse(ctx, MapClusterStatusCounts(results), "failed to calculate total status counts", err) +} + +func (h *APIHandler) GetTotalSeverityCounts(ctx *gin.Context) { + results, err := h.store.FetchTotalSeverityCounts(ctx, ctx.Param("source"), api.BuildFilter(ctx)) + + api.SendResponse(ctx, MapClusterSeverityCounts(results), "failed to calculate total status counts", err) +} + +func (h *APIHandler) ListClusterKinds(ctx *gin.Context) { + kinds, err := h.store.FetchClusterKinds(ctx, api.BuildFilter(ctx)) + + api.SendResponse(ctx, kinds, "failed to load cluster kinds", err) +} + +func (h *APIHandler) ListNamespaceKinds(ctx *gin.Context) { + kinds, err := h.store.FetchNamespaceKinds(ctx, api.BuildFilter(ctx)) + + api.SendResponse(ctx, kinds, "failed to load namespaced kinds", err) +} + +func (h *APIHandler) ListResourceResults(ctx *gin.Context) { + list, err := h.store.FetchResourceResults(ctx, ctx.Param("id"), api.BuildFilter(ctx)) + + api.SendResponse(ctx, MapResourceResults(list), "failed to load resource result list", err) +} + +func (h *APIHandler) ListResourcePolilcyResults(ctx *gin.Context) { + filter := api.BuildFilter(ctx) + list, err := h.store.FetchResourcePolicyResults(ctx, ctx.Param("id"), filter, api.BuildPagination(ctx, defaultOrder)) + if err != nil { + zap.L().Error("failed to load resource results", zap.Error(err)) + ctx.AbortWithStatus(http.StatusInternalServerError) + return + } + count, err := h.store.CountResourcePolicyResults(ctx, ctx.Param("id"), filter) + + api.SendResponse(ctx, Paginated[PolicyResult]{Count: count, Items: MapPolicyResults(list)}, "failed to load resource result list", err) +} + +func (h *APIHandler) ListPolicyResults(namespaced bool) gin.HandlerFunc { + return func(ctx *gin.Context) { + filter := api.BuildFilter(ctx) + + list, err := h.store.FetchResults(ctx, namespaced, filter, api.BuildPagination(ctx, defaultOrder)) + if err != nil { + zap.L().Error("failed to load results", zap.Error(err)) + ctx.AbortWithStatus(http.StatusInternalServerError) + return + } + count, err := h.store.CountResults(ctx, namespaced, filter) + + api.SendResponse(ctx, Paginated[PolicyResult]{Count: count, Items: MapPolicyResults(list)}, "failed to load resource result list", err) + } +} + +func (h *APIHandler) ListResultsWithoutResource(ctx *gin.Context) { + filter := api.BuildFilter(ctx) + + list, err := h.store.FetchResultsWithoutResource(ctx, filter, api.BuildPagination(ctx, defaultOrder)) + if err != nil { + zap.L().Error("failed to load results without resources", zap.Error(err)) + ctx.AbortWithStatus(http.StatusInternalServerError) + return + } + count, err := h.store.CountResultsWithoutResource(ctx, filter) + + api.SendResponse(ctx, Paginated[PolicyResult]{Count: count, Items: MapPolicyResults(list)}, "failed to load result list without resources", err) +} + +func (h *APIHandler) UseResources(ctx *gin.Context) { + resources, err := h.store.UseResources(ctx, ctx.Param("source"), api.BuildFilter(ctx)) + + api.SendResponse(ctx, gin.H{"resources": resources}, "failed to check if resources are used", err) +} + +func (h *APIHandler) ListFindings(ctx *gin.Context) { + results, err := h.store.FetchFindingCounts(ctx, api.BuildFilter(ctx)) + + api.SendResponse(ctx, MapFindings(results), "failed to load findings", err) +} + +func (h *APIHandler) ListSeverityFindings(ctx *gin.Context) { + results, err := h.store.FetchSeverityFindingCounts(ctx, api.BuildFilter(ctx)) + + api.SendResponse(ctx, MapSeverityFindings(results), "failed to load findings", err) +} + +func (h *APIHandler) ListTargets(ctx *gin.Context) { + api.SendResponse(ctx, h.targets, "failed to load findings", nil) +} + +func NewAPIHandler(store *db.Store, client namespaces.Client, targets map[string][]*Target) *APIHandler { + return &APIHandler{ + store: store, + nsClient: client, + targets: targets, + } +} + +func WithAPI(store *db.Store, client namespaces.Client, targets target.Targets) api.ServerOption { + return func(s *api.Server) error { + return s.Register("v2", NewAPIHandler(store, client, MapConfigTagrgets(targets))) + } +} diff --git a/pkg/api/v2/api_test.go b/pkg/api/v2/api_test.go new file mode 100644 index 00000000..3998121f --- /dev/null +++ b/pkg/api/v2/api_test.go @@ -0,0 +1,481 @@ +package v2_test + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/patrickmn/go-cache" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" + v1 "k8s.io/client-go/kubernetes/typed/core/v1" + + "github.com/kyverno/policy-reporter/pkg/api" + v2 "github.com/kyverno/policy-reporter/pkg/api/v2" + "github.com/kyverno/policy-reporter/pkg/database" + "github.com/kyverno/policy-reporter/pkg/fixtures" + "github.com/kyverno/policy-reporter/pkg/kubernetes/namespaces" + "github.com/kyverno/policy-reporter/pkg/report/result" + "github.com/kyverno/policy-reporter/pkg/target" +) + +const ( + nsDefault = "default" + nsTest = "test" +) + +func newFakeClient() v1.NamespaceInterface { + return fake.NewSimpleClientset( + &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: nsDefault, + Labels: map[string]string{ + "team": "team-a", + "group": "all", + }, + }, + }, + &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: nsTest, + Labels: map[string]string{ + "team": "team-b", + "group": "all", + }, + }, + }, + ).CoreV1().Namespaces() +} + +var reconditioner = result.NewReconditioner(nil) + +func TestV2(t *testing.T) { + db, err := database.NewSQLiteDB("db_v2.db") + if err != nil { + assert.Fail(t, "failed to init SQLite DB") + } + + store, err := database.NewStore(db, "1.0") + if err != nil { + assert.Fail(t, "failed to init Store") + } + + if err := store.PrepareDatabase(context.Background()); err != nil { + assert.Fail(t, "failed to prepare Store") + } + + store.Add(context.Background(), reconditioner.Prepare(fixtures.DefaultPolicyReport)) + store.Add(context.Background(), reconditioner.Prepare(fixtures.KyvernoPolicyReport)) + store.Add(context.Background(), reconditioner.Prepare(fixtures.KyvernoClusterPolicyReport)) + + client := namespaces.NewClient(newFakeClient(), cache.New(time.Second, time.Second)) + + gin.SetMode(gin.ReleaseMode) + + server := api.NewServer(gin.New(), v2.WithAPI(store, client, target.Targets{ + Webhook: &target.Config[target.WebhookOptions]{ + Name: "Webhook", + MinimumSeverity: "warn", + Config: &target.WebhookOptions{ + Webhook: "http://localhost:8080", + }, + }, + })) + + t.Run("TargetResponse", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v2/targets", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + }) + + t.Run("ResolveNamespaces", func(t *testing.T) { + body := new(bytes.Buffer) + body.Write([]byte(`{"team":"team-a"}`)) + + req, _ := http.NewRequest("POST", "/v2/namespaces/resolve-selector", body) + w := httptest.NewRecorder() + + server.Serve(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + resp := make([]string, 0, 1) + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, 1, len(resp)) + }) + + t.Run("ListNamespaces", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v2/namespaces", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := make([]string, 0, 1) + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, 2, len(resp)) + assert.Contains(t, resp, "test") + assert.Contains(t, resp, "kyverno") + } + }) + + t.Run("ListSources", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v2/sources", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := make([]string, 0, 1) + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, 2, len(resp)) + assert.Contains(t, resp, "test") + assert.Contains(t, resp, "Kyverno") + } + }) + + t.Run("ListPolicies", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v2/policies", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := make([]v2.Policy, 0, 1) + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, 4, len(resp)) + assert.Contains(t, resp, v2.Policy{Source: "test", Category: "Other", Name: "priority-test", Severity: "", Results: map[string]int{"fail": 1}}) + assert.Contains(t, resp, v2.Policy{Source: "Kyverno", Category: "test", Name: "cluster-required-quota", Severity: "high", Results: map[string]int{"fail": 1}}) + assert.Contains(t, resp, v2.Policy{Source: "Kyverno", Category: "test", Name: "required-limit", Severity: "high", Results: map[string]int{"pass": 1, "warn": 1}}) + assert.Contains(t, resp, v2.Policy{Source: "test", Category: "test", Name: "required-label", Severity: "high", Results: map[string]int{"fail": 2}}) + } + }) + + t.Run("UseResources", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v2/sources/Kyverno/use-resources", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := make(map[string]bool) + + json.NewDecoder(w.Body).Decode(&resp) + + assert.True(t, resp["resources"]) + } + }) + + t.Run("ListSourceWithCategories", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v2/sources/categories", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := make([]v2.SourceDetails, 0) + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Contains(t, resp, v2.SourceDetails{Name: "Kyverno", Categories: []*v2.Category{{Name: "test", Status: &v2.StatusList{Pass: 1, Warn: 1, Fail: 1}, Severities: &v2.SeverityList{High: 3}}}}) + assert.Contains(t, resp, v2.SourceDetails{Name: "test", Categories: []*v2.Category{{Name: "Other", Status: &v2.StatusList{Fail: 1}, Severities: &v2.SeverityList{Unknown: 1}}, {Name: "test", Status: &v2.StatusList{Fail: 2}, Severities: &v2.SeverityList{High: 2}}}}) + } + }) + + t.Run("ListResourceCategories", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v2/resource/17962226559046503697/source-categories", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := make([]v2.SourceDetails, 0) + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, resp[0], v2.SourceDetails{Name: "test", Categories: []*v2.Category{{Name: "test", Status: &v2.StatusList{Fail: 1}}}}) + } + }) + + t.Run("GetResource", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v2/resource/17962226559046503697", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := v2.Resource{} + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, resp, v2.Resource{ID: "17962226559046503697", UID: "dfd57c50-f30c-4729-b63f-b1954d8988d1", Namespace: "test", Name: "nginx", Kind: "Deployment", APIVersion: "v1"}) + } + }) + + t.Run("GetResourceStatusCounts", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v2/resource/17962226559046503697/status-counts", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := make([]v2.ResourceStatusCount, 0) + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Contains(t, resp, v2.ResourceStatusCount{Source: "test", Fail: 1}) + } + }) + + t.Run("ListNamespaceResourceResults", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v2/namespace-scoped/resource-results?namespaces=kyverno", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := v2.Paginated[v2.ResourceResult]{} + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, resp.Count, 2) + assert.Contains(t, resp.Items, v2.ResourceResult{ID: "6274512523942114905", UID: "dfd57c50-f30c-4729-b63f-b1954d8988d1", Name: "nginx", Kind: "Deployment", APIVersion: "v1", Namespace: "kyverno", Status: v2.Status{Pass: 1}, Severities: v2.Severities{High: 1}}) + assert.Contains(t, resp.Items, v2.ResourceResult{ID: "8277600851619588241", UID: "dfd57c50-f30c-4729-b63f-b1954d8988d2", Name: "nginx2", Kind: "Deployment", APIVersion: "v1", Namespace: "kyverno", Status: v2.Status{Warn: 1}, Severities: v2.Severities{High: 1}}) + } + }) + + t.Run("ListClusterResourceResults", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v2/cluster-scoped/resource-results", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := v2.Paginated[v2.ResourceResult]{} + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, resp.Count, 1) + assert.Equal(t, resp.Items[0], v2.ResourceResult{ID: "11786270724827677857", UID: "dfd57c50-f30c-4729-b63f-b1954d8988d1", Name: "kyverno", Kind: "Namespace", APIVersion: "v1", Source: "", Status: v2.Status{Fail: 1}, Severities: v2.Severities{High: 1}}) + } + }) + + t.Run("GetClusterStatusCounts", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v2/cluster-scoped/Kyverno/status-counts", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := make(map[string]int, 0) + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, 5, len(resp)) + assert.Equal(t, 1, resp["fail"]) + } + }) + + t.Run("GetNamespaceStatusCounts", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v2/namespace-scoped/Kyverno/status-counts", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := make(map[string]map[string]int, 0) + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, 1, len(resp)) + assert.Equal(t, 5, len(resp["kyverno"])) + assert.Equal(t, 1, resp["kyverno"]["pass"]) + assert.Equal(t, 1, resp["kyverno"]["warn"]) + assert.Equal(t, 0, resp["kyverno"]["fail"]) + assert.Equal(t, 0, resp["kyverno"]["error"]) + assert.Equal(t, 0, resp["kyverno"]["skip"]) + } + }) + + t.Run("ListClusterKinds", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v2/cluster-scoped/kinds", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := make([]string, 0) + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, 1, len(resp)) + assert.Equal(t, "Namespace", resp[0]) + } + }) + + t.Run("ListNamespaceKinds", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v2/namespace-scoped/kinds", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := make([]string, 0) + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, 1, len(resp)) + assert.Equal(t, "Deployment", resp[0]) + } + }) + + t.Run("ListResourceResults", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v2/resource/6274512523942114905/resource-results", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := make([]v2.ResourceResult, 0) + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, 1, len(resp)) + assert.Equal(t, resp[0], v2.ResourceResult{ID: "6274512523942114905", UID: "dfd57c50-f30c-4729-b63f-b1954d8988d1", Name: "nginx", Kind: "Deployment", APIVersion: "v1", Namespace: "kyverno", Source: "Kyverno", Status: v2.Status{Pass: 1}}) + } + }) + + t.Run("ListResourcePolilcyResults", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v2/resource/6274512523942114905/results", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := v2.Paginated[v2.PolicyResult]{} + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, 1, resp.Count) + assert.Equal(t, resp.Items[0], v2.PolicyResult{ID: "14158407137220160684", ResourceID: "6274512523942114905", Severity: "high", Name: "nginx", Kind: "Deployment", APIVersion: "v1", Namespace: "kyverno", Message: "message", Category: "test", Policy: "required-limit", Rule: "resource-limit-required", Status: "pass", Timestamp: 1614093003}) + } + }) + + t.Run("ListPolicyResults Namespaced", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v2/namespace-scoped/results?namespaces=kyverno", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := v2.Paginated[v2.PolicyResult]{} + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, 2, resp.Count) + assert.Equal(t, resp.Items[0], v2.PolicyResult{ID: "14158407137220160684", ResourceID: "6274512523942114905", Severity: "high", Name: "nginx", Kind: "Deployment", APIVersion: "v1", Namespace: "kyverno", Message: "message", Category: "test", Policy: "required-limit", Rule: "resource-limit-required", Status: "pass", Timestamp: 1614093003}) + assert.Equal(t, resp.Items[1], v2.PolicyResult{ID: "2079631062832497014", ResourceID: "8277600851619588241", Severity: "high", Name: "nginx2", Kind: "Deployment", APIVersion: "v1", Namespace: "kyverno", Message: "message", Category: "test", Policy: "required-limit", Rule: "resource-limit-required", Status: "warn", Timestamp: 1614093003}) + } + }) + + t.Run("ListPolicyResults", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v2/cluster-scoped/results", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := v2.Paginated[v2.PolicyResult]{} + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, 1, resp.Count) + assert.Equal(t, resp.Items[0], v2.PolicyResult{ID: "16800058481201255747", ResourceID: "11786270724827677857", Severity: "high", Name: "kyverno", Kind: "Namespace", APIVersion: "v1", Namespace: "", Message: "message", Category: "test", Policy: "cluster-required-quota", Rule: "ns-quota-required", Status: "fail", Timestamp: 1614093000}) + } + }) + + t.Run("ListResultsWithoutResource", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v2/results-without-resources", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := v2.Paginated[v2.PolicyResult]{} + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, 1, resp.Count) + assert.Equal(t, resp.Items[0], v2.PolicyResult{ID: "8115731892871392633", ResourceID: "18007334074686647077", Severity: "", Name: "", Kind: "", APIVersion: "", Namespace: "test", Message: "message 2", Category: "Other", Policy: "priority-test", Rule: "", Status: "fail", Timestamp: 1614093000}) + } + }) + + t.Run("UseResources", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v2/sources/Kyverno/use-resources", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := make(map[string]bool, 0) + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, 1, len(resp)) + assert.True(t, resp["resources"]) + } + }) + + t.Run("ListFindings", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v2/findings", nil) + w := httptest.NewRecorder() + + server.Serve(w, req) + + if ok := assert.Equal(t, http.StatusOK, w.Code); ok { + resp := v2.Findings{} + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, 6, resp.Total) + assert.Equal(t, 4, resp.PerResult["fail"]) + assert.Equal(t, 1, resp.PerResult["pass"]) + assert.Equal(t, 1, resp.PerResult["warn"]) + assert.Equal(t, 2, len(resp.Counts)) + assert.Contains(t, resp.Counts, &v2.FindingCounts{ + Total: 3, + Source: "Kyverno", + Counts: map[string]int{ + "fail": 1, + "pass": 1, + "warn": 1, + }, + }) + assert.Contains(t, resp.Counts, &v2.FindingCounts{ + Total: 3, + Source: "test", + Counts: map[string]int{ + "fail": 3, + }, + }) + } + }) +} diff --git a/pkg/api/v2/views.go b/pkg/api/v2/views.go new file mode 100644 index 00000000..0cdd9f8c --- /dev/null +++ b/pkg/api/v2/views.go @@ -0,0 +1,754 @@ +package v2 + +import ( + "fmt" + "net/url" + + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" + db "github.com/kyverno/policy-reporter/pkg/database" + "github.com/kyverno/policy-reporter/pkg/helper" + "github.com/kyverno/policy-reporter/pkg/target" +) + +type StatusList struct { + Pass int `json:"pass"` + Skip int `json:"skip"` + Warn int `json:"warn"` + Error int `json:"error"` + Fail int `json:"fail"` +} + +type SeverityList struct { + Unknown int `json:"unknown"` + Low int `json:"low"` + Info int `json:"info"` + Medium int `json:"medium"` + High int `json:"high"` + Critical int `json:"critical"` +} + +type Category struct { + Name string `json:"name"` + Status *StatusList `json:"status"` + Severities *SeverityList `json:"severities"` +} + +type SourceDetails struct { + Name string `json:"name"` + Categories []*Category `json:"categories"` +} + +func MapToSourceDetails(categories []db.Category) []*SourceDetails { + list := make(map[string]*SourceDetails, 0) + + for _, r := range categories { + if s, ok := list[r.Source]; ok { + UpdateCategory(r, s) + continue + } + + list[r.Source] = &SourceDetails{ + Name: r.Source, + Categories: []*Category{{ + Name: helper.Defaults(r.Name, "Other"), + Status: &StatusList{}, + Severities: &SeverityList{}, + }}, + } + + UpdateCategory(r, list[r.Source]) + } + + return helper.ToList(list) +} + +func UpdateCategory(result db.Category, source *SourceDetails) { + for _, c := range source.Categories { + if c.Name == helper.Defaults(result.Name, "Other") { + MapResultToCategory(result, c) + MapSeverityToCategory(result, c) + return + } + } + + category := &Category{ + Name: helper.Defaults(result.Name, "Other"), + Status: &StatusList{}, + Severities: &SeverityList{}, + } + + category = MapResultToCategory(result, category) + category = MapSeverityToCategory(result, category) + + source.Categories = append(source.Categories, category) +} + +func MapResultToCategory(result db.Category, category *Category) *Category { + switch result.Result { + case v1alpha2.StatusPass: + category.Status.Pass += result.Count + case v1alpha2.StatusWarn: + category.Status.Warn += result.Count + case v1alpha2.StatusFail: + category.Status.Fail += result.Count + case v1alpha2.StatusError: + category.Status.Error += result.Count + case v1alpha2.StatusSkip: + category.Status.Skip += result.Count + } + + return category +} + +func MapSeverityToCategory(result db.Category, category *Category) *Category { + switch result.Severity { + case v1alpha2.SeverityLow: + category.Severities.Low += result.Count + case v1alpha2.SeverityInfo: + category.Severities.Info += result.Count + case v1alpha2.SeverityMedium: + category.Severities.Medium += result.Count + case v1alpha2.SeverityHigh: + category.Severities.High += result.Count + case v1alpha2.SeverityCritical: + category.Severities.Critical += result.Count + default: + category.Severities.Unknown += result.Count + } + + return category +} + +type Resource struct { + ID string `json:"id,omitempty"` + UID string `json:"uid,omitempty"` + Name string `json:"name,omitempty"` + Namespace string `json:"namespace,omitempty"` + Kind string `json:"kind,omitempty"` + APIVersion string `json:"apiVersion,omitempty"` +} + +func MapResource(result db.ResourceResult) Resource { + return Resource{ + ID: result.ID, + UID: result.Resource.UID, + APIVersion: result.Resource.APIVersion, + Kind: result.Resource.Kind, + Name: result.Resource.Name, + Namespace: result.Resource.Namespace, + } +} + +type ResourceStatusCount struct { + Source string `json:"source,omitempty"` + Pass int `json:"pass"` + Warn int `json:"warn"` + Fail int `json:"fail"` + Error int `json:"error"` + Skip int `json:"skip"` +} + +func MapResourceStatusCounts(results []db.ResourceStatusCount) []ResourceStatusCount { + list := make([]ResourceStatusCount, 0, len(results)) + for _, result := range results { + list = append(list, ResourceStatusCount{ + Source: result.Source, + Pass: result.Pass, + Fail: result.Fail, + Warn: result.Warn, + Error: result.Error, + Skip: result.Skip, + }) + } + + return list +} + +type ResourceSeverityCount struct { + Source string `json:"source,omitempty"` + Info int `json:"info"` + Low int `json:"low"` + Medium int `json:"medium"` + High int `json:"high"` + Critical int `json:"critical"` + Unknown int `json:"unknown"` +} + +func MapResourceSeverityCounts(results []db.ResourceSeverityCount) []ResourceSeverityCount { + list := make([]ResourceSeverityCount, 0, len(results)) + for _, result := range results { + list = append(list, ResourceSeverityCount{ + Source: result.Source, + Info: result.Info, + Low: result.Low, + Medium: result.Medium, + High: result.High, + Critical: result.Critical, + Unknown: result.Unknown, + }) + } + + return list +} + +type Status struct { + Pass int `json:"pass"` + Skip int `json:"skip"` + Warn int `json:"warn"` + Fail int `json:"fail"` + Error int `json:"error"` +} + +type Severities struct { + Info int `json:"info"` + Low int `json:"low"` + Medium int `json:"medium"` + High int `json:"high"` + Critical int `json:"critical"` + Unknown int `json:"unknown"` +} + +type ResourceResult struct { + ID string `json:"id"` + UID string `json:"uid"` + Name string `json:"name"` + Kind string `json:"kind"` + APIVersion string `json:"apiVersion"` + Namespace string `json:"namespace,omitempty"` + Source string `json:"source,omitempty"` + Status Status `json:"status"` + Severities Severities `json:"severities"` +} + +func MapResourceResults(results []db.ResourceResult) []ResourceResult { + return helper.Map(results, func(res db.ResourceResult) ResourceResult { + return ResourceResult{ + ID: res.ID, + UID: res.Resource.UID, + Namespace: res.Resource.Namespace, + Kind: res.Resource.Kind, + APIVersion: res.Resource.APIVersion, + Name: res.Resource.Name, + Source: res.Source, + Status: Status{ + Pass: res.Pass, + Skip: res.Skip, + Warn: res.Warn, + Fail: res.Fail, + Error: res.Error, + }, + Severities: Severities{ + Unknown: res.Unknown, + Info: res.Info, + Low: res.Low, + Medium: res.Medium, + High: res.High, + Critical: res.Critical, + }, + } + }) +} + +type Paginated[T any] struct { + Items []T `json:"items"` + Count int `json:"count"` +} + +type StatusCount struct { + Namespace string `json:"namespace,omitempty"` + Source string `json:"source,omitempty"` + Status string `json:"status"` + Count int `json:"count"` +} + +func MapClusterSeverityCounts(results []db.SeverityCount) map[string]int { + mapping := map[string]int{ + "unknown": 0, + v1alpha2.SeverityLow: 0, + v1alpha2.SeverityInfo: 0, + v1alpha2.SeverityMedium: 0, + v1alpha2.SeverityHigh: 0, + v1alpha2.SeverityCritical: 0, + } + + for _, result := range results { + mapping[result.Severity] = result.Count + } + + return mapping +} + +func MapClusterStatusCounts(results []db.StatusCount) map[string]int { + mapping := map[string]int{ + v1alpha2.StatusPass: 0, + v1alpha2.StatusFail: 0, + v1alpha2.StatusWarn: 0, + v1alpha2.StatusError: 0, + v1alpha2.StatusSkip: 0, + } + + for _, result := range results { + mapping[result.Status] = result.Count + } + + return mapping +} + +func MapNamespaceStatusCounts(results []db.StatusCount) map[string]map[string]int { + mapping := map[string]map[string]int{} + + for _, result := range results { + if _, ok := mapping[result.Namespace]; !ok { + mapping[result.Namespace] = map[string]int{ + v1alpha2.StatusPass: 0, + v1alpha2.StatusFail: 0, + v1alpha2.StatusWarn: 0, + v1alpha2.StatusError: 0, + v1alpha2.StatusSkip: 0, + } + } + + mapping[result.Namespace][result.Status] = result.Count + } + + return mapping +} + +func MapNamespaceSeverityCounts(results []db.SeverityCount) map[string]map[string]int { + mapping := map[string]map[string]int{} + + for _, result := range results { + if _, ok := mapping[result.Namespace]; !ok { + mapping[result.Namespace] = map[string]int{ + "unknown": 0, + v1alpha2.SeverityInfo: 0, + v1alpha2.SeverityLow: 0, + v1alpha2.SeverityMedium: 0, + v1alpha2.SeverityHigh: 0, + v1alpha2.SeverityCritical: 0, + } + } + + mapping[result.Namespace][result.Severity] = result.Count + } + + return mapping +} + +type Policy struct { + Source string `json:"source,omitempty"` + Category string `json:"category,omitempty"` + Name string `json:"policy"` + Severity string `json:"severity,omitempty"` + Results map[string]int `json:"results"` +} + +func MapPolicies(results []db.PolicyReportFilter) []*Policy { + list := make(map[string]*Policy) + + for _, r := range results { + category := r.Category + if category == "" { + category = "Other" + } + + if _, ok := list[r.Policy]; ok { + list[r.Policy].Results[r.Result] = r.Count + continue + } + + list[r.Policy] = &Policy{ + Source: r.Source, + Category: category, + Name: r.Policy, + Severity: r.Severity, + Results: map[string]int{ + r.Result: r.Count, + }, + } + } + + return helper.ToList(list) +} + +type PolicyResult struct { + ID string `json:"id"` + Namespace string `json:"namespace,omitempty"` + Kind string `json:"kind"` + APIVersion string `json:"apiVersion"` + Name string `json:"name"` + ResourceID string `json:"resourceId"` + Message string `json:"message"` + Category string `json:"category,omitempty"` + Policy string `json:"policy"` + Rule string `json:"rule"` + Status string `json:"status"` + Severity string `json:"severity,omitempty"` + Timestamp int64 `json:"timestamp,omitempty"` + Properties map[string]string `json:"properties,omitempty"` +} + +func MapPolicyResults(results []db.PolicyReportResult) []PolicyResult { + return helper.Map(results, func(res db.PolicyReportResult) PolicyResult { + return PolicyResult{ + ID: res.ID, + Namespace: res.Resource.Namespace, + Kind: res.Resource.Kind, + APIVersion: res.Resource.APIVersion, + Name: res.Resource.Name, + ResourceID: res.Resource.GetID(), + Message: res.Message, + Category: res.Category, + Policy: res.Policy, + Rule: res.Rule, + Status: res.Result, + Severity: res.Severity, + Timestamp: res.Created, + Properties: res.Properties, + } + }) +} + +type FindingCounts struct { + Total int `json:"total"` + Source string `json:"source"` + Counts map[string]int `json:"counts"` +} + +type Findings struct { + Total int `json:"total"` + PerResult map[string]int `json:"perResult"` + Counts []*FindingCounts `json:"counts"` +} + +func MapFindings(results []db.StatusCount) Findings { + findings := make(map[string]*FindingCounts, 0) + totals := make(map[string]int, 5) + total := 0 + + for _, count := range results { + if finding, ok := findings[count.Source]; ok { + finding.Counts[count.Status] = count.Count + finding.Total = finding.Total + count.Count + } else { + findings[count.Source] = &FindingCounts{ + Source: count.Source, + Total: count.Count, + Counts: map[string]int{ + count.Status: count.Count, + }, + } + } + + totals[count.Status] += count.Count + total += count.Count + } + + return Findings{Counts: helper.ToList(findings), Total: total, PerResult: totals} +} + +func MapSeverityFindings(results []db.SeverityCount) Findings { + findings := make(map[string]*FindingCounts, 0) + totals := make(map[string]int, 5) + total := 0 + + for _, count := range results { + if finding, ok := findings[count.Source]; ok { + finding.Counts[count.Severity] = count.Count + finding.Total = finding.Total + count.Count + } else { + findings[count.Source] = &FindingCounts{ + Source: count.Source, + Total: count.Count, + Counts: map[string]int{ + count.Severity: count.Count, + }, + } + } + + totals[count.Severity] += count.Count + total += count.Count + } + + return Findings{Counts: helper.ToList(findings), Total: total, PerResult: totals} +} + +func MapResourceCategoryToSourceDetails(categories []db.ResourceCategory) []*SourceDetails { + list := make(map[string]*SourceDetails, 0) + + for _, r := range categories { + if s, ok := list[r.Source]; ok { + s.Categories = append(s.Categories, &Category{ + Name: r.Name, + Status: &StatusList{ + Pass: r.Pass, + Fail: r.Fail, + Warn: r.Warn, + Error: r.Error, + Skip: r.Skip, + }, + }) + continue + } + + list[r.Source] = &SourceDetails{ + Name: r.Source, + Categories: []*Category{{ + Name: r.Name, + Status: &StatusList{ + Pass: r.Pass, + Fail: r.Fail, + Warn: r.Warn, + Error: r.Error, + Skip: r.Skip, + }, + }}, + } + } + + return helper.ToList(list) +} + +type ValueFilter struct { + Include []string `json:"include,omitempty"` + Exclude []string `json:"exclude,omitempty"` + Selector map[string]any `json:"selector,omitempty"` +} + +type TargetFilter struct { + Namespaces *ValueFilter `json:"namespaces,omitempty"` + Severities *ValueFilter `json:"severities,omitempty"` + Status *ValueFilter `json:"status,omitempty"` + Policies *ValueFilter `json:"policies,omitempty"` + ReportLabels *ValueFilter `json:"reportLabels,omitempty"` + Sources *ValueFilter `json:"sources,omitempty"` +} + +type Target struct { + Name string `json:"name"` + Type string `json:"type"` + SecretRef string `json:"secretRef,omitempty"` + MountedSecret string `json:"mountedSecret,omitempty"` + MinimumSeverity string `json:"minimumSeverity"` + Filter TargetFilter `json:"filter"` + CustomFields map[string]string `json:"customFields"` + Properties map[string]any `json:"properties"` + Host string `json:"host,omitempty"` + SkipTLS bool `json:"skipTLS,omitempty"` + UseTLS bool `json:"useTLS,omitempty"` + Auth bool `json:"auth"` +} + +func MapValueFilter(f target.ValueFilter) *ValueFilter { + if len(f.Exclude)+len(f.Include) == 0+len(f.Selector) { + return nil + } + + return &ValueFilter{ + Include: f.Include, + Exclude: f.Exclude, + Selector: f.Selector, + } +} + +func MapBaseToTarget[T any](t *target.Config[T]) *Target { + fields := t.CustomFields + if fields == nil { + fields = make(map[string]string, 0) + } + + return &Target{ + Name: t.Name, + MinimumSeverity: t.MinimumSeverity, + SecretRef: t.SecretRef, + MountedSecret: t.MountedSecret, + CustomFields: fields, + Properties: make(map[string]any), + Filter: TargetFilter{ + Namespaces: MapValueFilter(t.Filter.Namespaces), + Severities: MapValueFilter(t.Filter.Severities), + Status: MapValueFilter(t.Filter.Status), + Policies: MapValueFilter(t.Filter.Policies), + ReportLabels: MapValueFilter(t.Filter.ReportLabels), + Sources: MapValueFilter(target.ValueFilter{ + Include: t.Sources, + }), + }, + } +} + +func MapSlackToTarget(ta *target.Config[target.SlackOptions]) *Target { + t := MapBaseToTarget(ta) + t.Type = "Slack" + t.Properties["channel"] = ta.Config.Channel + + return t +} + +func MapLokiToTarget(ta *target.Config[target.LokiOptions]) *Target { + t := MapBaseToTarget(ta) + t.Type = "Loki" + t.Host = ta.Config.Host + t.SkipTLS = ta.Config.SkipTLS + t.UseTLS = ta.Config.Certificate != "" + t.Properties["api"] = ta.Config.Path + + if v, ok := ta.Config.Headers["Authorization"]; ok && v != "" { + t.Auth = true + } else if ta.Config.Username != "" && ta.Config.Password != "" { + t.Auth = true + } + + return t +} + +func MapElasticsearchToTarget(ta *target.Config[target.ElasticsearchOptions]) *Target { + t := MapBaseToTarget(ta) + t.Type = "Elasticsearch" + t.Host = ta.Config.Host + t.SkipTLS = ta.Config.SkipTLS + t.UseTLS = ta.Config.Certificate != "" + t.Auth = (ta.Config.Username != "" && ta.Config.Password != "") || ta.Config.APIKey != "" + t.Properties["rotation"] = ta.Config.Rotation + t.Properties["index"] = ta.Config.Index + + if v, ok := ta.Config.Headers["Authorization"]; ok && v != "" { + t.Auth = true + } + + return t +} + +func MapWebhhokToTarget(typeName string) func(ta *target.Config[target.WebhookOptions]) *Target { + return func(ta *target.Config[target.WebhookOptions]) *Target { + t := MapBaseToTarget(ta) + t.Type = typeName + t.SkipTLS = ta.Config.SkipTLS + t.UseTLS = ta.Config.Certificate != "" + + if u, err := url.Parse(ta.Config.Webhook); err == nil { + t.Host = fmt.Sprintf("%s://%s", u.Scheme, u.Host) + t.Auth = u.User != nil + } + + if v, ok := ta.Config.Headers["Authorization"]; ok && v != "" { + t.Auth = true + } + + return t + } +} + +func MapTelegramToTarget(ta *target.Config[target.TelegramOptions]) *Target { + t := MapBaseToTarget(ta) + t.Type = "Telegram" + t.Host = ta.Config.Webhook + t.SkipTLS = ta.Config.SkipTLS + t.UseTLS = ta.Config.Certificate != "" + t.Properties["chatId"] = ta.Config.ChatID + + return t +} + +func MapS3ToTarget(ta *target.Config[target.S3Options]) *Target { + t := MapBaseToTarget(ta) + t.Type = "S3" + t.Host = ta.Config.Endpoint + t.Properties["prefix"] = ta.Config.Prefix + t.Properties["bucket"] = ta.Config.Bucket + t.Properties["region"] = ta.Config.Region + t.Auth = true + + return t +} + +func MapKinesisToTarget(ta *target.Config[target.KinesisOptions]) *Target { + t := MapBaseToTarget(ta) + t.Type = "Kinesis" + t.Host = ta.Config.Endpoint + t.Properties["stream"] = ta.Config.StreamName + t.Properties["region"] = ta.Config.Region + t.Auth = true + + return t +} + +func MapSecurityHubToTarget(ta *target.Config[target.SecurityHubOptions]) *Target { + t := MapBaseToTarget(ta) + t.Type = "SecurityHub" + t.Host = ta.Config.Endpoint + t.Properties["region"] = ta.Config.Region + t.Properties["synchronize"] = ta.Config.Synchronize + t.Auth = true + + return t +} + +func MapGCSToTarget(ta *target.Config[target.GCSOptions]) *Target { + t := MapBaseToTarget(ta) + t.Type = "GoogleCloudStore" + t.Properties["prefix"] = ta.Config.Prefix + t.Properties["bucket"] = ta.Config.Bucket + t.Auth = true + + return t +} + +func MapTargets[T any](c *target.Config[T], mapper func(*target.Config[T]) *Target) []*Target { + targets := make([]*Target, 0) + + if c == nil { + return targets + } + + if c.Valid { + targets = append(targets, mapper(c)) + } + + for _, channel := range c.Channels { + if channel.Valid { + targets = append(targets, mapper(channel)) + } + } + + return targets +} + +func MapConfigTagrgets(c target.Targets) map[string][]*Target { + targets := make(map[string][]*Target) + + targets["loki"] = MapTargets(c.Loki, MapLokiToTarget) + targets["elasticsearch"] = MapTargets(c.Elasticsearch, MapElasticsearchToTarget) + targets["slack"] = MapTargets(c.Slack, MapSlackToTarget) + targets["discord"] = MapTargets(c.Discord, MapWebhhokToTarget("Discord")) + targets["teams"] = MapTargets(c.Teams, MapWebhhokToTarget("MS Teams")) + targets["googleChat"] = MapTargets(c.GoogleChat, MapWebhhokToTarget("GoogleChat")) + targets["webhook"] = MapTargets(c.Webhook, MapWebhhokToTarget("Webhook")) + targets["telegram"] = MapTargets(c.Telegram, MapTelegramToTarget) + targets["s3"] = MapTargets(c.S3, MapS3ToTarget) + targets["kinesis"] = MapTargets(c.Kinesis, MapKinesisToTarget) + targets["securityHub"] = MapTargets(c.SecurityHub, MapSecurityHubToTarget) + targets["gcs"] = MapTargets(c.GCS, MapGCSToTarget) + + for k, v := range targets { + if len(v) == 0 { + delete(targets, k) + } + } + + return targets +} + +type ResultProperty struct { + Namespace string `json:"namespace"` + Property string `json:"property"` +} + +func MapResultPropertyList(results []db.ResultProperty) []ResultProperty { + return helper.Map(results, func(res db.ResultProperty) ResultProperty { + return ResultProperty{ + Namespace: res.Namespace, + Property: res.Property, + } + }) +} diff --git a/pkg/api/v2/views_test.go b/pkg/api/v2/views_test.go new file mode 100644 index 00000000..0730ab6a --- /dev/null +++ b/pkg/api/v2/views_test.go @@ -0,0 +1,362 @@ +package v2_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + v2 "github.com/kyverno/policy-reporter/pkg/api/v2" + "github.com/kyverno/policy-reporter/pkg/database" + "github.com/kyverno/policy-reporter/pkg/target" +) + +func TestV2Views(t *testing.T) { + t.Run("MapValueFilter", func(t *testing.T) { + empty := v2.MapValueFilter(target.ValueFilter{}) + + assert.Nil(t, empty) + + original := target.ValueFilter{ + Include: []string{"default"}, + Exclude: []string{"kube-system"}, + Selector: map[string]any{"team": "marketing"}, + } + + filter := v2.MapValueFilter(original) + + assert.Equal(t, original.Include, filter.Include) + assert.Equal(t, original.Exclude, filter.Exclude) + assert.Equal(t, original.Selector, filter.Selector) + }) + + t.Run("MapResourceCategoryToSourceDetails", func(t *testing.T) { + result := v2.MapResourceCategoryToSourceDetails([]database.ResourceCategory{ + { + Source: "Kyverno", + Name: "PSS Baseline", + Pass: 8, + Fail: 3, + }, + { + Source: "Kyverno", + Name: "PSS Restricted", + Pass: 4, + Fail: 1, + }, + { + Source: "Trivy", + Name: "Vulnr", + Pass: 0, + Fail: 2, + Warn: 4, + }, + }) + + assert.Equal(t, 2, len(result)) + assert.Contains(t, result, &v2.SourceDetails{Name: "Kyverno", Categories: []*v2.Category{ + { + Name: "PSS Baseline", + Status: &v2.StatusList{ + Pass: 8, + Fail: 3, + }, + }, + { + Name: "PSS Restricted", + Status: &v2.StatusList{ + Pass: 4, + Fail: 1, + }, + }, + }}) + assert.Contains(t, result, &v2.SourceDetails{Name: "Trivy", Categories: []*v2.Category{ + { + Name: "Vulnr", + Status: &v2.StatusList{ + Pass: 0, + Fail: 2, + Warn: 4, + }, + }, + }}) + }) + + t.Run("MapBaseToTarget", func(t *testing.T) { + target := v2.MapBaseToTarget(&target.Config[target.WebhookOptions]{ + Name: "Webhook", + MinimumSeverity: "medium", + SecretRef: "ref", + MountedSecret: "mounted", + Sources: []string{"Kyverno"}, + SkipExisting: true, + Valid: true, + }) + + assert.Equal(t, "Webhook", target.Name) + assert.Equal(t, "medium", target.MinimumSeverity) + assert.Equal(t, "ref", target.SecretRef) + assert.Equal(t, "mounted", target.MountedSecret) + assert.NotNil(t, target.CustomFields) + assert.NotNil(t, target.Properties) + assert.Equal(t, []string{"Kyverno"}, target.Filter.Sources.Include) + }) + + t.Run("MapSlackToTarget", func(t *testing.T) { + target := v2.MapSlackToTarget(&target.Config[target.SlackOptions]{ + Name: "Slack", + MinimumSeverity: "medium", + Config: &target.SlackOptions{ + Channel: "general", + WebhookOptions: target.WebhookOptions{ + Webhook: "http://slack.com/xxxx", + }, + }, + Valid: true, + }) + + assert.Equal(t, "Slack", target.Name) + assert.Equal(t, "medium", target.MinimumSeverity) + assert.Equal(t, "Slack", target.Type) + assert.Equal(t, "general", target.Properties["channel"]) + }) + + t.Run("MapLokiToTarget", func(t *testing.T) { + target := v2.MapLokiToTarget(&target.Config[target.LokiOptions]{ + Name: "Loki 1", + MinimumSeverity: "medium", + Config: &target.LokiOptions{ + HostOptions: target.HostOptions{ + Host: "http://loki.monitoring:3000", + Certificate: "cert", + SkipTLS: true, + }, + Username: "user", + Password: "password", + Path: "v1/push", + }, + Valid: true, + }) + + assert.Equal(t, "Loki 1", target.Name) + assert.Equal(t, "medium", target.MinimumSeverity) + + assert.Equal(t, "Loki", target.Type) + assert.Equal(t, "v1/push", target.Properties["api"]) + assert.Equal(t, "http://loki.monitoring:3000", target.Host) + assert.True(t, target.SkipTLS) + assert.True(t, target.UseTLS) + assert.True(t, target.Auth) + }) + + t.Run("MapElasticsearchToTarget", func(t *testing.T) { + target := v2.MapElasticsearchToTarget(&target.Config[target.ElasticsearchOptions]{ + Name: "Target", + MinimumSeverity: "medium", + Config: &target.ElasticsearchOptions{ + HostOptions: target.HostOptions{ + Host: "http://elasticsearch.monitoring:3000", + Certificate: "cert", + SkipTLS: true, + Headers: map[string]string{ + "Authorization": "Bearer 123456", + }, + }, + Index: "policy-reporter", + Rotation: "daily", + }, + Valid: true, + }) + + assert.Equal(t, "Target", target.Name) + assert.Equal(t, "medium", target.MinimumSeverity) + + assert.Equal(t, "Elasticsearch", target.Type) + assert.Equal(t, "policy-reporter", target.Properties["index"]) + assert.Equal(t, "daily", target.Properties["rotation"]) + assert.Equal(t, "http://elasticsearch.monitoring:3000", target.Host) + assert.True(t, target.SkipTLS) + assert.True(t, target.UseTLS) + assert.True(t, target.Auth) + }) + + t.Run("MapWebhhokToTarget", func(t *testing.T) { + target := v2.MapWebhhokToTarget("Discord")(&target.Config[target.WebhookOptions]{ + Name: "Target", + MinimumSeverity: "medium", + Config: &target.WebhookOptions{ + Webhook: "http://discord.com/12345/888XABC", + Certificate: "cert", + SkipTLS: true, + Headers: map[string]string{ + "Authorization": "Bearer 123456", + }, + }, + Valid: true, + }) + + assert.Equal(t, "Target", target.Name) + assert.Equal(t, "medium", target.MinimumSeverity) + + assert.Equal(t, "Discord", target.Type) + assert.Equal(t, "http://discord.com", target.Host) + assert.True(t, target.SkipTLS) + assert.True(t, target.UseTLS) + assert.True(t, target.Auth) + }) + + t.Run("MapTelegramToTarget", func(t *testing.T) { + target := v2.MapTelegramToTarget(&target.Config[target.TelegramOptions]{ + Name: "Target", + MinimumSeverity: "medium", + Config: &target.TelegramOptions{ + Token: "ABCDE", + ChatID: "1234567", + WebhookOptions: target.WebhookOptions{ + Webhook: "http://telegram.com", + Certificate: "cert", + SkipTLS: true, + }, + }, + Valid: true, + }) + + assert.Equal(t, "Target", target.Name) + assert.Equal(t, "medium", target.MinimumSeverity) + + assert.Equal(t, "Telegram", target.Type) + assert.Equal(t, "http://telegram.com", target.Host) + assert.Equal(t, "1234567", target.Properties["chatId"]) + assert.True(t, target.SkipTLS) + assert.True(t, target.UseTLS) + assert.False(t, target.Auth) + }) + + t.Run("MapS3ToTarget", func(t *testing.T) { + target := v2.MapS3ToTarget(&target.Config[target.S3Options]{ + Name: "Target", + MinimumSeverity: "medium", + Config: &target.S3Options{ + Prefix: "policy-reporter", + Bucket: "kyverno", + AWSConfig: target.AWSConfig{ + Region: "eu-central-1", + Endpoint: "https://s3.aws.com", + }, + }, + Valid: true, + }) + + assert.Equal(t, "Target", target.Name) + assert.Equal(t, "medium", target.MinimumSeverity) + + assert.Equal(t, "S3", target.Type) + assert.Equal(t, "https://s3.aws.com", target.Host) + assert.Equal(t, "kyverno", target.Properties["bucket"]) + assert.Equal(t, "policy-reporter", target.Properties["prefix"]) + assert.Equal(t, "eu-central-1", target.Properties["region"]) + assert.True(t, target.Auth) + }) + + t.Run("MapKinesisToTarget", func(t *testing.T) { + target := v2.MapKinesisToTarget(&target.Config[target.KinesisOptions]{ + Name: "Target", + MinimumSeverity: "medium", + Config: &target.KinesisOptions{ + StreamName: "policy-reporter", + AWSConfig: target.AWSConfig{ + Region: "eu-central-1", + Endpoint: "https://kinesis.aws.com", + }, + }, + Valid: true, + }) + + assert.Equal(t, "Target", target.Name) + assert.Equal(t, "medium", target.MinimumSeverity) + + assert.Equal(t, "Kinesis", target.Type) + assert.Equal(t, "https://kinesis.aws.com", target.Host) + assert.Equal(t, "policy-reporter", target.Properties["stream"]) + assert.Equal(t, "eu-central-1", target.Properties["region"]) + assert.True(t, target.Auth) + }) + + t.Run("MapSecurityHubToTarget", func(t *testing.T) { + target := v2.MapSecurityHubToTarget(&target.Config[target.SecurityHubOptions]{ + Name: "Target", + MinimumSeverity: "medium", + Config: &target.SecurityHubOptions{ + AccountID: "policy-reporter", + Synchronize: true, + AWSConfig: target.AWSConfig{ + Region: "eu-central-1", + Endpoint: "https://securityhub.aws.com", + }, + }, + Valid: true, + }) + + assert.Equal(t, "Target", target.Name) + assert.Equal(t, "medium", target.MinimumSeverity) + + assert.Equal(t, "SecurityHub", target.Type) + assert.Equal(t, "https://securityhub.aws.com", target.Host) + assert.Equal(t, "eu-central-1", target.Properties["region"]) + assert.Equal(t, true, target.Properties["synchronize"]) + assert.True(t, target.Auth) + }) + + t.Run("MapGCSToTarget", func(t *testing.T) { + target := v2.MapGCSToTarget(&target.Config[target.GCSOptions]{ + Name: "Target", + MinimumSeverity: "medium", + Config: &target.GCSOptions{ + Prefix: "policy-reporter", + Bucket: "kyverno", + }, + Valid: true, + }) + + assert.Equal(t, "Target", target.Name) + assert.Equal(t, "medium", target.MinimumSeverity) + + assert.Equal(t, "GoogleCloudStore", target.Type) + assert.Equal(t, "kyverno", target.Properties["bucket"]) + assert.Equal(t, "policy-reporter", target.Properties["prefix"]) + assert.True(t, target.Auth) + }) + + t.Run("MapTargets", func(t *testing.T) { + targets := v2.MapTargets(&target.Config[target.GCSOptions]{ + Name: "Target", + MinimumSeverity: "medium", + Config: &target.GCSOptions{ + Prefix: "policy-reporter", + Bucket: "kyverno", + }, + Valid: true, + Channels: []*target.Config[target.GCSOptions]{ + { + Name: "Target 2", + MinimumSeverity: "medium", + Config: &target.GCSOptions{ + Prefix: "policy-reporter", + Bucket: "trivy", + }, + Valid: true, + }, + { + Name: "Target 2", + MinimumSeverity: "medium", + Config: &target.GCSOptions{ + Prefix: "policy-reporter", + Bucket: "trivy", + }, + Valid: false, + }, + }, + }, v2.MapGCSToTarget) + + assert.Equal(t, 2, len(targets)) + }) +} diff --git a/pkg/cache/redis_test.go b/pkg/cache/redis_test.go index 11565fa2..bf91391c 100644 --- a/pkg/cache/redis_test.go +++ b/pkg/cache/redis_test.go @@ -7,6 +7,7 @@ import ( "time" goredis "github.com/go-redis/redis/v8" + "github.com/kyverno/policy-reporter/pkg/cache" "github.com/kyverno/policy-reporter/pkg/fixtures" ) diff --git a/pkg/config/config.go b/pkg/config/config.go index 0b59c404..7324d9f6 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -3,278 +3,84 @@ package config import "github.com/kyverno/policy-reporter/pkg/target" type ValueFilter struct { - Include []string `mapstructure:"include" json:"include,omitempty"` - Exclude []string `mapstructure:"exclude" json:"exclude,omitempty"` + Include []string `mapstructure:"include"` + Exclude []string `mapstructure:"exclude"` + Selector map[string]any `mapstructure:"selector"` } type EmailReportFilter struct { - DisableClusterReports bool `mapstructure:"disableClusterReports" json:"disableClusterReports,omitempty"` - Namespaces ValueFilter `mapstructure:"namespaces" json:"namespaces,omitempty"` - Sources ValueFilter `mapstructure:"sources" json:"sources,omitempty"` + DisableClusterReports bool `mapstructure:"disableClusterReports"` + Namespaces ValueFilter `mapstructure:"namespaces"` + Sources ValueFilter `mapstructure:"sources"` } type TargetFilter struct { - Namespaces ValueFilter `mapstructure:"namespaces" json:"namespaces,omitempty"` - Priorities ValueFilter `mapstructure:"priorities" json:"priorities,omitempty"` - Policies ValueFilter `mapstructure:"policies" json:"policies,omitempty"` - ReportLabels ValueFilter `mapstructure:"reportLabels" json:"reportLabels,omitempty"` + Namespaces ValueFilter `mapstructure:"namespaces"` + Priorities ValueFilter `mapstructure:"priorities"` + Policies ValueFilter `mapstructure:"policies"` + ReportLabels ValueFilter `mapstructure:"reportLabels"` } type MetricsFilter struct { - Namespaces ValueFilter `mapstructure:"namespaces" json:"namespaces,omitempty"` - Policies ValueFilter `mapstructure:"policies" json:"policies,omitempty"` - Severities ValueFilter `mapstructure:"severities" json:"severities,omitempty"` - Status ValueFilter `mapstructure:"status" json:"status,omitempty"` - Sources ValueFilter `mapstructure:"sources" json:"sources,omitempty"` - Kinds ValueFilter `mapstructure:"kinds" json:"kinds,omitempty"` -} - -type TargetBaseOptions struct { - Name string `mapstructure:"name" json:"name,omitempty"` - MinimumPriority string `mapstructure:"minimumPriority" json:"minimumPriority,omitempty"` - Filter TargetFilter `mapstructure:"filter" json:"filter,omitempty"` - SecretRef string `mapstructure:"secretRef" json:"secretRef,omitempty"` - MountedSecret string `mapstructure:"mountedSecret" json:"mountedSecret,omitempty"` - Sources []string `mapstructure:"sources" json:"sources,omitempty"` - CustomFields map[string]string `mapstructure:"customFields" json:"customFields,omitempty"` - SkipExisting bool `mapstructure:"skipExistingOnStartup" json:"skipExistingOnStartup,omitempty"` -} - -func (config *TargetBaseOptions) MapBaseParent(parent TargetBaseOptions) { - if config.MinimumPriority == "" { - config.MinimumPriority = parent.MinimumPriority - } - - if !config.SkipExisting { - config.SkipExisting = parent.SkipExisting - } -} - -func (config *TargetBaseOptions) ClientOptions() target.ClientOptions { - return target.ClientOptions{ - Name: config.Name, - SkipExistingOnStartup: config.SkipExisting, - ResultFilter: createResultFilter(config.Filter, config.MinimumPriority, config.Sources), - ReportFilter: createReportFilter(config.Filter), - } -} - -type AWSConfig struct { - AccessKeyID string `mapstructure:"accessKeyID" json:"accessKeyID,omitempty"` - SecretAccessKey string `mapstructure:"secretAccessKey" json:"secretAccessKey,omitempty"` - Region string `mapstructure:"region" json:"region,omitempty"` - Endpoint string `mapstructure:"endpoint" json:"endpoint,omitempty"` -} - -func (config *AWSConfig) MapAWSParent(parent AWSConfig) { - if config.Endpoint == "" { - config.Endpoint = parent.Endpoint - } - - if config.AccessKeyID == "" { - config.AccessKeyID = parent.AccessKeyID - } - - if config.SecretAccessKey == "" { - config.SecretAccessKey = parent.SecretAccessKey - } - - if config.Region == "" { - config.Region = parent.Region - } -} - -type TargetOption interface { - BaseOptions() *TargetBaseOptions -} - -// Loki configuration -type Loki struct { - TargetBaseOptions `mapstructure:",squash" json:",inline"` - CustomLabels map[string]string `mapstructure:"customLabels" json:"customLabels,omitempty"` - Headers map[string]string `mapstructure:"headers" json:"headers,omitempty"` - Host string `mapstructure:"host" json:"host,omitempty"` - SkipTLS bool `mapstructure:"skipTLS" json:"skipTLS,omitempty"` - Certificate string `mapstructure:"certificate" json:"certificate,omitempty"` - Path string `mapstructure:"path" json:"path,omitempty"` - Channels []*Loki `mapstructure:"channels" json:"channels,omitempty"` - Username string `mapstructure:"username" json:"username,omitempty"` - Password string `mapstructure:"password" json:"password,omitempty"` -} - -// Elasticsearch configuration -type Elasticsearch struct { - TargetBaseOptions `mapstructure:",squash" json:",inline"` - Host string `mapstructure:"host" json:"host,omitempty"` - SkipTLS bool `mapstructure:"skipTLS" json:"skipTLS,omitempty"` - Certificate string `mapstructure:"certificate" json:"certificate,omitempty"` - Index string `mapstructure:"index" json:"index,omitempty"` - Rotation string `mapstructure:"rotation" json:"rotation,omitempty"` - Username string `mapstructure:"username" json:"username,omitempty"` - Password string `mapstructure:"password" json:"password,omitempty"` - APIKey string `mapstructure:"apiKey" json:"apiKey,omitempty"` - Channels []*Elasticsearch `mapstructure:"channels" json:"channels,omitempty"` - TypelessAPI bool `mapstructure:"typelessApi" json:"typelessApi,omitempty"` -} - -// Slack configuration -type Slack struct { - TargetBaseOptions `mapstructure:",squash" json:",inline"` - Webhook string `mapstructure:"webhook" json:"webhook,omitempty"` - Channel string `mapstructure:"channel" json:"channel,omitempty"` - Channels []*Slack `mapstructure:"channels" json:"channels,omitempty"` -} - -// Discord configuration -type Discord struct { - TargetBaseOptions `mapstructure:",squash" json:",inline"` - Webhook string `mapstructure:"webhook" json:"webhook,omitempty"` - Channels []*Discord `mapstructure:"channels" json:"channels,omitempty"` -} - -// Teams configuration -type Teams struct { - TargetBaseOptions `mapstructure:",squash" json:",inline"` - Webhook string `mapstructure:"webhook" json:"webhook,omitempty"` - SkipTLS bool `mapstructure:"skipTLS" json:"skipTLS,omitempty"` - Certificate string `mapstructure:"certificate" json:"certificate,omitempty"` - Channels []*Teams `mapstructure:"channels" json:"channels,omitempty"` -} - -// UI configuration -type UI struct { - TargetBaseOptions `mapstructure:",squash" json:",inline"` - Host string `mapstructure:"host" json:"host,omitempty"` - SkipTLS bool `mapstructure:"skipTLS" json:"skipTLS,omitempty"` - Certificate string `mapstructure:"certificate" json:"certificate,omitempty"` -} - -// Webhook configuration -type Webhook struct { - TargetBaseOptions `mapstructure:",squash" json:",inline"` - Host string `mapstructure:"host" json:"host,omitempty"` - SkipTLS bool `mapstructure:"skipTLS" json:"skipTLS,omitempty"` - Certificate string `mapstructure:"certificate" json:"certificate,omitempty"` - Headers map[string]string `mapstructure:"headers" json:"headers,omitempty"` - Channels []*Webhook `mapstructure:"channels" json:"channels,omitempty"` -} - -// Telegram configuration -type Telegram struct { - TargetBaseOptions `mapstructure:",squash" json:",inline"` - Host string `mapstructure:"host"` - Token string `mapstructure:"token"` - ChatID string `mapstructure:"chatID"` - SkipTLS bool `mapstructure:"skipTLS"` - Certificate string `mapstructure:"certificate"` - Headers map[string]string `mapstructure:"headers"` - Channels []*Telegram `mapstructure:"channels"` -} - -// GoogleChat configuration -type GoogleChat struct { - TargetBaseOptions `mapstructure:",squash" json:",inline"` - Webhook string `mapstructure:"webhook" json:"webhook,omitempty"` - SkipTLS bool `mapstructure:"skipTLS" json:"skipTLS,omitempty"` - Certificate string `mapstructure:"certificate" json:"certificate,omitempty"` - Headers map[string]string `mapstructure:"headers" json:"headers,omitempty"` - Channels []*GoogleChat `mapstructure:"channels" json:"channels,omitempty"` -} - -// S3 configuration -type S3 struct { - TargetBaseOptions `mapstructure:",squash" json:",inline"` - AWSConfig `mapstructure:",squash" json:",inline"` - Prefix string `mapstructure:"prefix" json:"prefix,omitempty"` - Bucket string `mapstructure:"bucket" json:"bucket,omitempty"` - BucketKeyEnabled bool `mapstructure:"bucketKeyEnabled" json:"bucketKeyEnabled,omitempty"` - KmsKeyID string `mapstructure:"kmsKeyId" json:"kmsKeyId,omitempty"` - ServerSideEncryption string `mapstructure:"serverSideEncryption" json:"serverSideEncryption,omitempty"` - PathStyle bool `mapstructure:"pathStyle" json:"pathStyle,omitempty"` - Channels []*S3 `mapstructure:"channels" json:"channels,omitempty"` -} - -// Kinesis configuration -type Kinesis struct { - TargetBaseOptions `mapstructure:",squash" json:",inline"` - AWSConfig `mapstructure:",squash" json:",inline"` - StreamName string `mapstructure:"streamName" json:"streamName,omitempty"` - Channels []*Kinesis `mapstructure:"channels" json:"channels,omitempty"` -} - -// SecurityHub configuration -type SecurityHub struct { - TargetBaseOptions `mapstructure:",squash" json:",inline"` - AWSConfig `mapstructure:",squash" json:",inline"` - AccountID string `mapstructure:"accountId" json:"accountId,omitempty"` - ProductName string `mapstructure:"productName" json:"productName,omitempty"` - CompanyName string `mapstructure:"companyName" json:"companyName,omitempty"` - DelayInSeconds int `mapstructure:"delayInSeconds" json:"delayInSeconds,omitempty"` - Cleanup bool `mapstructure:"cleanup" json:"cleanup,omitempty"` - Channels []*SecurityHub `mapstructure:"channels" json:"channels,omitempty"` -} - -// GCS configuration -type GCS struct { - TargetBaseOptions `mapstructure:",squash" json:",inline"` - Credentials string `mapstructure:"credentials" json:"customLabels,omitempty"` - Prefix string `mapstructure:"prefix" json:"credentials,omitempty"` - Bucket string `mapstructure:"bucket" json:"bucket,omitempty"` - Sources []string `mapstructure:"sources" json:"sources,omitempty"` - Channels []*GCS `mapstructure:"channels" json:"channels,omitempty"` + Namespaces ValueFilter `mapstructure:"namespaces"` + Policies ValueFilter `mapstructure:"policies"` + Severities ValueFilter `mapstructure:"severities"` + Status ValueFilter `mapstructure:"status"` + Sources ValueFilter `mapstructure:"sources"` + Kinds ValueFilter `mapstructure:"kinds"` } // SMTP configuration type SMTP struct { - Host string `mapstructure:"host" json:"host,omitempty"` - Port int `mapstructure:"port" json:"port,omitempty"` - Username string `mapstructure:"username" json:"username,omitempty"` - Password string `mapstructure:"password" json:"password,omitempty"` - From string `mapstructure:"from" json:"from,omitempty"` - Encryption string `mapstructure:"encryption" json:"encryption,omitempty"` - SkipTLS bool `mapstructure:"skipTLS" json:"skipTLS,omitempty"` - Certificate string `mapstructure:"certificate" json:"certificate,omitempty"` + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` + Username string `mapstructure:"username"` + Password string `mapstructure:"password"` + From string `mapstructure:"from"` + Encryption string `mapstructure:"encryption"` + SkipTLS bool `mapstructure:"skipTLS"` + Certificate string `mapstructure:"certificate"` } // EmailReport configuration type EmailReport struct { - To []string `mapstructure:"to" json:"to,omitempty"` - Format string `mapstructure:"format" json:"format,omitempty"` - Filter EmailReportFilter `mapstructure:"filter" json:"filter,omitempty"` - Channels []EmailReport `mapstructure:"channels" json:"channels,omitempty"` + To []string `mapstructure:"to"` + Format string `mapstructure:"format"` + Filter EmailReportFilter `mapstructure:"filter"` + Channels []EmailReport `mapstructure:"channels"` } // EmailReport configuration type Templates struct { - Dir string `mapstructure:"dir" json:"dir,omitempty"` + Dir string `mapstructure:"dir"` } // EmailReports configuration type EmailReports struct { - SMTP SMTP `mapstructure:"smtp" json:"smtp,omitempty"` - Summary EmailReport `mapstructure:"summary" json:"summary,omitempty"` - Violations EmailReport `mapstructure:"violations" json:"violations,omitempty"` - ClusterName string `mapstructure:"clusterName" json:"clusterName,omitempty"` - TitlePrefix string `mapstructure:"titlePrefix" json:"titlePrefix,omitempty"` + SMTP SMTP `mapstructure:"smtp"` + Summary EmailReport `mapstructure:"summary"` + Violations EmailReport `mapstructure:"violations"` + ClusterName string `mapstructure:"clusterName"` + TitlePrefix string `mapstructure:"titlePrefix"` } // BasicAuth configuration type BasicAuth struct { - Username string `mapstructure:"username" json:"username,omitempty"` - Password string `mapstructure:"password" json:"password,omitempty"` - SecretRef string `mapstructure:"secretRef" json:"secretRef,omitempty"` + Username string `mapstructure:"username"` + Password string `mapstructure:"password"` + SecretRef string `mapstructure:"secretRef"` } // API configuration type API struct { - Port int `mapstructure:"port" json:"port,omitempty"` - Logging bool `mapstructure:"logging" json:"logging,omitempty"` - BasicAuth BasicAuth `mapstructure:"basicAuth" json:"basicAuth,omitempty"` + Port int `mapstructure:"port"` + BasicAuth BasicAuth `mapstructure:"basicAuth"` + DebugMode bool `mapstructure:"debug"` } // REST configuration type REST struct { - Enabled bool `mapstructure:"enabled" json:"enabled,omitempty"` + Enabled bool `mapstructure:"enabled"` } // Metrics configuration @@ -287,106 +93,107 @@ type Metrics struct { // Profiling configuration type Profiling struct { - Enabled bool `mapstructure:"enabled" json:"enabled,omitempty"` -} - -// ClusterReportFilter configuration -type ClusterReportFilter struct { - Disabled bool `mapstructure:"disabled" json:"disabled,omitempty"` + Enabled bool `mapstructure:"enabled"` } // ReportFilter configuration type ReportFilter struct { - Namespaces ValueFilter `mapstructure:"namespaces" json:"namespaces,omitempty"` - ClusterReports ClusterReportFilter `mapstructure:"clusterReports" json:"clusterReports,omitempty"` + Namespaces ValueFilter `mapstructure:"namespaces"` + Sources ValueFilter `mapstructure:"sources"` + Kinds ValueFilter `mapstructure:"kinds"` + DisableClusterReports bool `mapstructure:"disableClusterReports"` } // Redis configuration type Redis struct { - Enabled bool `mapstructure:"enabled" json:"enabled,omitempty"` - Address string `mapstructure:"address" json:"address,omitempty"` - Prefix string `mapstructure:"prefix" json:"prefix,omitempty"` - Username string `mapstructure:"username" json:"username,omitempty"` - Password string `mapstructure:"password" json:"password,omitempty"` - Database int `mapstructure:"database" json:"database,omitempty"` + Enabled bool `mapstructure:"enabled"` + Address string `mapstructure:"address"` + Prefix string `mapstructure:"prefix"` + Username string `mapstructure:"username"` + Password string `mapstructure:"password"` + Database int `mapstructure:"database"` } // LeaderElection configuration type LeaderElection struct { - LockName string `mapstructure:"lockName" json:"lockName,omitempty"` - PodName string `mapstructure:"podName" json:"podName,omitempty"` - Namespace string `mapstructure:"namespace" json:"namespace,omitempty"` - LeaseDuration int `mapstructure:"leaseDuration" json:"leaseDuration,omitempty"` - RenewDeadline int `mapstructure:"renewDeadline" json:"renewDeadline,omitempty"` - RetryPeriod int `mapstructure:"retryPeriod" json:"retryPeriod,omitempty"` - ReleaseOnCancel bool `mapstructure:"releaseOnCancel" json:"releaseOnCancel,omitempty"` - Enabled bool `mapstructure:"enabled" json:"enabled,omitempty"` + LockName string `mapstructure:"lockName"` + PodName string `mapstructure:"podName"` + Namespace string `mapstructure:"namespace"` + LeaseDuration int `mapstructure:"leaseDuration"` + RenewDeadline int `mapstructure:"renewDeadline"` + RetryPeriod int `mapstructure:"retryPeriod"` + ReleaseOnCancel bool `mapstructure:"releaseOnCancel"` + Enabled bool `mapstructure:"enabled"` } // K8sClient config struct type K8sClient struct { - QPS float32 `mapstructure:"qps" json:"qps,omitempty"` - Burst int `mapstructure:"burst" json:"burst,omitempty"` - Kubeconfig string `mapstructure:"kubeconfig" json:"kubeconfig,omitempty"` + QPS float32 `mapstructure:"qps"` + Burst int `mapstructure:"burst"` + Kubeconfig string `mapstructure:"kubeconfig"` } type Logging struct { - LogLevel int8 `mapstructure:"logLevel" json:"logLevel,omitempty"` - Encoding string `mapstructure:"encoding" json:"encoding,omitempty"` - Development bool `mapstructure:"development" json:"development,omitempty"` + Server bool `mapstructure:"server"` + LogLevel int8 `mapstructure:"logLevel"` + Encoding string `mapstructure:"encoding"` + Development bool `mapstructure:"development"` } type Database struct { - Type string `mapstructure:"type" json:"type,omitempty"` - DSN string `mapstructure:"dsn" json:"dsn,omitempty"` - Username string `mapstructure:"username" json:"username,omitempty"` - Password string `mapstructure:"password" json:"password,omitempty"` - Database string `mapstructure:"database" json:"database,omitempty"` - Host string `mapstructure:"host" json:"host,omitempty"` - EnableSSL bool `mapstructure:"enableSSL" json:"enableSSL,omitempty"` - SecretRef string `mapstructure:"secretRef" json:"secretRef,omitempty"` - MountedSecret string `mapstructure:"mountedSecret" json:"mountedSecret,omitempty"` + Type string `mapstructure:"type"` + DSN string `mapstructure:"dsn"` + Username string `mapstructure:"username"` + Password string `mapstructure:"password"` + Database string `mapstructure:"database"` + Host string `mapstructure:"host"` + EnableSSL bool `mapstructure:"enableSSL"` + SecretRef string `mapstructure:"secretRef"` + MountedSecret string `mapstructure:"mountedSecret"` +} + +type SourceSelector struct { + Source string `mapstructure:"source"` +} + +type SourceFilter struct { + Selector SourceSelector `mapstructure:"selector"` + Kinds ValueFilter `mapstructure:"kinds"` + Sources ValueFilter `mapstructure:"sources"` + Namespaces ValueFilter `mapstructure:"namespaces"` + UncontrolledOnly bool `mapstructure:"uncontrolledOnly"` + DisableClusterReports bool `mapstructure:"disableClusterReports"` } type CustomID struct { - Enabled bool `mapstructure:"enabled" json:"enabled,omitempty"` - Fields []string `mapstructure:"fields" json:"fields,omitempty"` + Enabled bool `mapstructure:"enabled"` + Fields []string `mapstructure:"fields"` } type SourceConfig struct { - CustomID `mapstructure:"customID" json:"customID,omitempty"` + Selector SourceSelector `mapstructure:"selector"` + CustomID `mapstructure:"customId"` } // Config of the PolicyReporter type Config struct { - Version string `json:"version,omitempty"` - Namespace string `mapstructure:"namespace" json:"namespace,omitempty"` - Loki *Loki `mapstructure:"loki" json:"loki,omitempty"` - Elasticsearch *Elasticsearch `mapstructure:"elasticsearch" json:"elasticsearch,omitempty"` - Slack *Slack `mapstructure:"slack" json:"slack,omitempty"` - Discord *Discord `mapstructure:"discord" json:"discord,omitempty"` - Teams *Teams `mapstructure:"teams" json:"teams,omitempty"` - S3 *S3 `mapstructure:"s3" json:"s3,omitempty"` - Kinesis *Kinesis `mapstructure:"kinesis" json:"kinesis,omitempty"` - SecurityHub *SecurityHub `mapstructure:"securityHub" json:"securityHub,omitempty"` - GCS *GCS `mapstructure:"gcs" json:"gcs,omitempty"` - UI *UI `mapstructure:"ui" json:"ui,omitempty"` - Webhook *Webhook `mapstructure:"webhook" json:"webhook,omitempty"` - Telegram *Telegram `mapstructure:"telegram" json:"telegram,omitempty"` - GoogleChat *GoogleChat `mapstructure:"googleChat" json:"googleChat,omitempty"` - API API `mapstructure:"api" json:"api,omitempty"` - WorkerCount int `mapstructure:"worker" json:"worker,omitempty"` - DBFile string `mapstructure:"dbfile" json:"dbfile,omitempty"` - Metrics Metrics `mapstructure:"metrics" json:"metrics,omitempty"` - REST REST `mapstructure:"rest" json:"rest,omitempty"` - ReportFilter ReportFilter `mapstructure:"reportFilter" json:"reportFilter,omitempty"` - Redis Redis `mapstructure:"redis" json:"redis,omitempty"` - Profiling Profiling `mapstructure:"profiling" json:"profiling,omitempty"` - EmailReports EmailReports `mapstructure:"emailReports" json:"emailReports,omitempty"` - LeaderElection LeaderElection `mapstructure:"leaderElection" json:"leaderElection,omitempty"` - K8sClient K8sClient `mapstructure:"k8sClient" json:"k8sClient,omitempty"` - Logging Logging `mapstructure:"logging" json:"logging,omitempty"` - Database Database `mapstructure:"database" json:"database,omitempty"` - SourceConfig map[string]SourceConfig `mapstructure:"sourceConfig" json:"sourceConfig,omitempty"` - Templates Templates `mapstructure:"templates" json:"templates,omitempty"` + Version string + Namespace string `mapstructure:"namespace"` + API API `mapstructure:"api"` + WorkerCount int `mapstructure:"worker"` + DBFile string `mapstructure:"dbfile"` + Metrics Metrics `mapstructure:"metrics"` + REST REST `mapstructure:"rest"` + ReportFilter ReportFilter `mapstructure:"reportFilter"` + SourceFilters []SourceFilter `mapstructure:"sourceFilters"` + Redis Redis `mapstructure:"redis"` + Profiling Profiling `mapstructure:"profiling"` + EmailReports EmailReports `mapstructure:"emailReports"` + LeaderElection LeaderElection `mapstructure:"leaderElection"` + K8sClient K8sClient `mapstructure:"k8sClient"` + Logging Logging `mapstructure:"logging"` + Database Database `mapstructure:"database"` + Targets target.Targets `mapstructure:"target"` + SourceConfig []SourceConfig `mapstructure:"sourceConfig"` + Templates Templates `mapstructure:"templates"` } diff --git a/pkg/config/database_factory.go b/pkg/config/database_factory.go index 03383b1e..85ead861 100644 --- a/pkg/config/database_factory.go +++ b/pkg/config/database_factory.go @@ -14,6 +14,7 @@ import ( "github.com/uptrace/bun/dialect/mysqldialect" "github.com/uptrace/bun/dialect/pgdialect" "github.com/uptrace/bun/driver/pgdriver" + "github.com/uptrace/bun/extra/bundebug" "go.uber.org/zap" "github.com/kyverno/policy-reporter/pkg/database" @@ -88,6 +89,8 @@ func (f *DatabaseFactory) NewSQLite(file string) *bun.DB { return nil } + sqldb.AddQueryHook(bundebug.NewQueryHook()) + return sqldb } diff --git a/pkg/config/database_factory_test.go b/pkg/config/database_factory_test.go index 89468905..543bbdc0 100644 --- a/pkg/config/database_factory_test.go +++ b/pkg/config/database_factory_test.go @@ -1,14 +1,54 @@ package config_test import ( + "encoding/json" + "os" "testing" "github.com/uptrace/bun/dialect" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8sfake "k8s.io/client-go/kubernetes/fake" + v1 "k8s.io/client-go/kubernetes/typed/core/v1" "github.com/kyverno/policy-reporter/pkg/config" "github.com/kyverno/policy-reporter/pkg/kubernetes/secrets" ) +const ( + secretName = "secret-values" + mountedSecret = "/tmp/secrets-9999" +) + +func newFakeClient() v1.SecretInterface { + return k8sfake.NewSimpleClientset(&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: "default", + }, + Data: map[string][]byte{ + "host": []byte("http://localhost:9200"), + "username": []byte("username"), + "password": []byte("password"), + "apiKey": []byte("apiKey"), + "database": []byte("database"), + "dsn": []byte(""), + }, + }).CoreV1().Secrets("default") +} + +func mountSecret() { + secretValues := secrets.Values{ + Host: "http://localhost:9200", + Username: "username", + Password: "password", + Database: "database", + DSN: "", + } + file, _ := json.MarshalIndent(secretValues, "", " ") + _ = os.WriteFile(mountedSecret, file, 0o644) +} + func Test_ResolveDatabase(t *testing.T) { factory := config.NewDatabaseFactory(nil) diff --git a/pkg/config/readinessprobe.go b/pkg/config/readinessprobe.go index 2f613fac..949502f3 100644 --- a/pkg/config/readinessprobe.go +++ b/pkg/config/readinessprobe.go @@ -31,12 +31,15 @@ func (r *ReadinessProbe) Ready() { func (r *ReadinessProbe) Wait() { if r.required() && !r.running { r.running = <-r.ready - close(r.ready) zap.L().Debug("readiness probe finished") return } } +func (r *ReadinessProbe) Close() { + close(r.ready) +} + func (r *ReadinessProbe) Running() bool { return r.running } diff --git a/pkg/config/resolver.go b/pkg/config/resolver.go index 2c0413ef..954eb412 100644 --- a/pkg/config/resolver.go +++ b/pkg/config/resolver.go @@ -8,8 +8,10 @@ import ( "strings" "time" + "github.com/gin-gonic/gin" goredis "github.com/go-redis/redis/v8" _ "github.com/mattn/go-sqlite3" + gocache "github.com/patrickmn/go-cache" "github.com/uptrace/bun" "github.com/uptrace/bun/dialect" mail "github.com/xhit/go-simple-mail/v2" @@ -30,6 +32,9 @@ import ( "github.com/kyverno/policy-reporter/pkg/email/violations" "github.com/kyverno/policy-reporter/pkg/helper" "github.com/kyverno/policy-reporter/pkg/kubernetes" + "github.com/kyverno/policy-reporter/pkg/kubernetes/jobs" + "github.com/kyverno/policy-reporter/pkg/kubernetes/namespaces" + "github.com/kyverno/policy-reporter/pkg/kubernetes/pods" "github.com/kyverno/policy-reporter/pkg/kubernetes/secrets" "github.com/kyverno/policy-reporter/pkg/leaderelection" "github.com/kyverno/policy-reporter/pkg/listener" @@ -37,6 +42,7 @@ import ( "github.com/kyverno/policy-reporter/pkg/report" "github.com/kyverno/policy-reporter/pkg/report/result" "github.com/kyverno/policy-reporter/pkg/target" + "github.com/kyverno/policy-reporter/pkg/target/factory" "github.com/kyverno/policy-reporter/pkg/validate" ) @@ -44,25 +50,22 @@ import ( type Resolver struct { config *Config k8sConfig *rest.Config + clientset *k8s.Clientset publisher report.EventPublisher policyStore *database.Store database *bun.DB policyReportClient report.PolicyReportClient leaderElector *leaderelection.Client resultCache cache.Cache - targetClients []target.Client + targetClients *target.Collection targetsCreated bool + targetFactory target.Factory logger *zap.Logger resultListener *listener.ResultListener } // APIServer resolver method -func (r *Resolver) APIServer(ctx context.Context, synced func() bool) api.Server { - var logger *zap.Logger - if r.config.API.Logging { - logger, _ = r.Logger() - } - +func (r *Resolver) Server(ctx context.Context, options []api.ServerOption) (*api.Server, error) { if r.config.API.BasicAuth.SecretRef != "" { values, err := r.SecretClient().Get(ctx, r.config.API.BasicAuth.SecretRef) if err != nil { @@ -77,23 +80,34 @@ func (r *Resolver) APIServer(ctx context.Context, synced func() bool) api.Server } } - var auth *api.BasicAuth + defaults := []api.ServerOption{ + api.WithGZIP(), + } + + if r.config.Logging.Server || r.config.API.DebugMode { + defaults = append(defaults, api.WithLogging(zap.L())) + } else { + defaults = append(defaults, api.WithRecovery()) + } + if r.config.API.BasicAuth.Username != "" && r.config.API.BasicAuth.Password != "" { - auth = &api.BasicAuth{ + defaults = append(defaults, api.WithBasicAuth(api.BasicAuth{ Username: r.config.API.BasicAuth.Username, Password: r.config.API.BasicAuth.Password, - } + })) zap.L().Info("API BasicAuth enabled") } - return api.NewServer( - r.TargetClients(), - r.config.API.Port, - logger, - auth, - synced, - ) + if r.config.Profiling.Enabled { + defaults = append(defaults, api.WithProfiling()) + } + + if !r.config.API.DebugMode { + gin.SetMode(gin.ReleaseMode) + } + + return api.NewServer(gin.New(), append(defaults, options...)...), nil } // Database resolver method @@ -128,7 +142,7 @@ func (r *Resolver) Database() *bun.DB { } // PolicyReportStore resolver method -func (r *Resolver) PolicyReportStore(db *bun.DB) (*database.Store, error) { +func (r *Resolver) Store(db *bun.DB) (*database.Store, error) { if r.policyStore != nil { return r.policyStore, nil } @@ -178,12 +192,12 @@ func (r *Resolver) EventPublisher() report.EventPublisher { func (r *Resolver) CustomIDGenerators() map[string]result.IDGenerator { generators := make(map[string]result.IDGenerator) - for s, c := range r.config.SourceConfig { + for _, c := range r.config.SourceConfig { if !c.Enabled || len(c.Fields) == 0 { continue } - generators[strings.ToLower(s)] = result.NewIDGenerator(c.Fields) + generators[strings.ToLower(c.Selector.Source)] = result.NewIDGenerator(c.Fields) } return generators @@ -196,10 +210,32 @@ func (r *Resolver) Queue() (*kubernetes.Queue, error) { return nil, err } + podsClient, err := r.PodClient() + if err != nil { + return nil, err + } + + jobsClient, err := r.JobClient() + if err != nil { + return nil, err + } + return kubernetes.NewQueue( kubernetes.NewDebouncer(1*time.Minute, r.EventPublisher()), - workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "report-queue"), + workqueue.NewTypedRateLimitingQueueWithConfig(workqueue.DefaultTypedControllerRateLimiter[string](), workqueue.TypedRateLimitingQueueConfig[string]{ + Name: "report-queue", + }), client, + report.NewSourceFilter(podsClient, jobsClient, helper.Map(r.config.SourceFilters, func(f SourceFilter) report.SourceValidation { + return report.SourceValidation{ + Selector: report.ReportSelector{Source: f.Selector.Source}, + Kinds: ToRuleSet(f.Kinds), + Sources: ToRuleSet(f.Sources), + Namespaces: ToRuleSet(f.Namespaces), + UncontrolledOnly: f.UncontrolledOnly, + DisableClusterReports: f.DisableClusterReports, + } + })), result.NewReconditioner(r.CustomIDGenerators()), ), nil } @@ -207,7 +243,7 @@ func (r *Resolver) Queue() (*kubernetes.Queue, error) { // RegisterNewResultsListener resolver method func (r *Resolver) RegisterNewResultsListener() { targets := r.TargetClients() - if len(targets) == 0 { + if targets.Empty() { return } @@ -222,7 +258,7 @@ func (r *Resolver) RegisterNewResultsListener() { func (r *Resolver) RegisterSendResultListener() { targets := r.TargetClients() - if len(targets) == 0 { + if targets.Empty() { return } @@ -231,9 +267,11 @@ func (r *Resolver) RegisterSendResultListener() { } r.resultListener.RegisterListener(listener.NewSendResultListener(targets)) + r.resultListener.RegisterScopeListener(listener.NewSendScopeResultsListener(targets)) + r.resultListener.RegisterSyncListener(listener.NewSendSyncResultsListener(targets)) } -// RegisterSendResultListener resolver method +// UnregisterSendResultListener resolver method func (r *Resolver) UnregisterSendResultListener() { if r.ResultCache().Shared() { r.EventPublisher().UnregisterListener(listener.NewResults) @@ -244,9 +282,10 @@ func (r *Resolver) UnregisterSendResultListener() { } r.resultListener.UnregisterListener() + r.resultListener.UnregisterScopeListener() } -// RegisterSendResultListener resolver method +// RegisterStoreListener resolver method func (r *Resolver) RegisterStoreListener(ctx context.Context, store report.PolicyReportStore) { r.EventPublisher().RegisterListener(listener.Store, listener.NewStoreListener(ctx, store)) } @@ -271,9 +310,25 @@ func (r *Resolver) RegisterMetricsListener() { )) } +// Clientset resolver method +func (r *Resolver) Clientset() (*k8s.Clientset, error) { + if r.clientset != nil { + return r.clientset, nil + } + + clientset, err := k8s.NewForConfig(r.k8sConfig) + if err != nil { + return nil, err + } + + r.clientset = clientset + + return r.clientset, nil +} + // SecretClient resolver method func (r *Resolver) SecretClient() secrets.Client { - clientset, err := k8s.NewForConfig(r.k8sConfig) + clientset, err := r.Clientset() if err != nil { return nil } @@ -281,10 +336,61 @@ func (r *Resolver) SecretClient() secrets.Client { return secrets.NewClient(clientset.CoreV1().Secrets(r.config.Namespace)) } -func (r *Resolver) TargetFactory() *TargetFactory { - return &TargetFactory{ - secretClient: r.SecretClient(), +// NamespaceClient resolver method +func (r *Resolver) NamespaceClient() (namespaces.Client, error) { + clientset, err := r.Clientset() + if err != nil { + return nil, err } + + return namespaces.NewClient( + clientset.CoreV1().Namespaces(), + gocache.New(15*time.Second, 5*time.Second), + ), nil +} + +// PodClient resolver method +func (r *Resolver) PodClient() (pods.Client, error) { + clientset, err := r.Clientset() + if err != nil { + return nil, err + } + + return pods.NewClient(clientset.CoreV1()), nil +} + +// JobClient resolver method +func (r *Resolver) JobClient() (jobs.Client, error) { + clientset, err := r.Clientset() + if err != nil { + return nil, err + } + + return jobs.NewClient(clientset.BatchV1()), nil +} + +func (r *Resolver) TargetFactory() target.Factory { + if r.targetFactory != nil { + return r.targetFactory + } + + ns, err := r.NamespaceClient() + if err != nil { + zap.L().Error("failed to create namespace client", zap.Error(err)) + } + + r.targetFactory = factory.NewFactory(r.SecretClient(), target.NewResultFilterFactory(ns)) + + return r.targetFactory +} + +func (r *Resolver) SecretInformer() (secrets.Informer, error) { + client, err := r.CRDMetadataClient() + if err != nil { + return nil, err + } + + return secrets.NewInformer(client, r.TargetFactory(), r.config.Namespace), nil } func (r *Resolver) DatabaseFactory() *DatabaseFactory { @@ -294,40 +400,19 @@ func (r *Resolver) DatabaseFactory() *DatabaseFactory { } // TargetClients resolver method -func (r *Resolver) TargetClients() []target.Client { +func (r *Resolver) TargetClients() *target.Collection { if r.targetsCreated { return r.targetClients } - factory := r.TargetFactory() - - clients := make([]target.Client, 0) - - clients = append(clients, factory.LokiClients(r.config.Loki)...) - clients = append(clients, factory.ElasticsearchClients(r.config.Elasticsearch)...) - clients = append(clients, factory.SlackClients(r.config.Slack)...) - clients = append(clients, factory.DiscordClients(r.config.Discord)...) - clients = append(clients, factory.TeamsClients(r.config.Teams)...) - clients = append(clients, factory.S3Clients(r.config.S3)...) - clients = append(clients, factory.KinesisClients(r.config.Kinesis)...) - clients = append(clients, factory.SecurityHubs(r.config.SecurityHub)...) - clients = append(clients, factory.WebhookClients(r.config.Webhook)...) - clients = append(clients, factory.GCSClients(r.config.GCS)...) - clients = append(clients, factory.TelegramClients(r.config.Telegram)...) - clients = append(clients, factory.GoogleChatClients(r.config.GoogleChat)...) - - if ui := factory.UIClient(r.config.UI); ui != nil { - clients = append(clients, ui) - } - - r.targetClients = clients + r.targetClients = r.TargetFactory().CreateClients(&r.config.Targets) r.targetsCreated = true return r.targetClients } func (r *Resolver) HasTargets() bool { - return len(r.TargetClients()) > 0 + return !r.TargetClients().Empty() } func (r *Resolver) EnableLeaderElection() bool { @@ -344,7 +429,7 @@ func (r *Resolver) EnableLeaderElection() bool { // SkipExistingOnStartup config method func (r *Resolver) SkipExistingOnStartup() bool { - for _, client := range r.TargetClients() { + for _, client := range r.TargetClients().Clients() { if !client.SkipExistingOnStartup() { return false } @@ -377,9 +462,14 @@ func (r *Resolver) SummaryGenerator() (*summary.Generator, error) { return nil, err } + nsclient, err := r.NamespaceClient() + if err != nil { + return nil, err + } + return summary.NewGenerator( client, - EmailReportFilterFromConfig(r.config.EmailReports.Summary.Filter), + EmailReportFilterFromConfig(nsclient, r.config.EmailReports.Summary.Filter), !r.config.EmailReports.Summary.Filter.DisableClusterReports, ), nil } @@ -398,9 +488,14 @@ func (r *Resolver) ViolationsGenerator() (*violations.Generator, error) { return nil, err } + nsclient, err := r.NamespaceClient() + if err != nil { + return nil, err + } + return violations.NewGenerator( client, - EmailReportFilterFromConfig(r.config.EmailReports.Violations.Filter), + EmailReportFilterFromConfig(nsclient, r.config.EmailReports.Violations.Filter), !r.config.EmailReports.Violations.Filter.DisableClusterReports, ), nil } @@ -466,9 +561,9 @@ func (r *Resolver) PolicyReportClient() (report.PolicyReportClient, error) { return r.policyReportClient, nil } -func (r *Resolver) ReportFilter() *report.Filter { - return report.NewFilter( - r.config.ReportFilter.ClusterReports.Disabled, +func (r *Resolver) ReportFilter() *report.MetaFilter { + return report.NewMetaFilter( + r.config.ReportFilter.DisableClusterReports, ToRuleSet(r.config.ReportFilter.Namespaces), ) } @@ -554,13 +649,18 @@ func NewResolver(config *Config, k8sConfig *rest.Config) Resolver { } } -func EmailReportFilterFromConfig(config EmailReportFilter) email.Filter { - return email.NewFilter(ToRuleSet(config.Namespaces), ToRuleSet(config.Sources)) +func EmailReportFilterFromConfig(client namespaces.Client, config EmailReportFilter) email.Filter { + return email.NewFilter( + client, + ToRuleSet(config.Namespaces), + ToRuleSet(config.Sources), + ) } func ToRuleSet(filter ValueFilter) validate.RuleSets { return validate.RuleSets{ - Include: filter.Include, - Exclude: filter.Exclude, + Include: filter.Include, + Exclude: filter.Exclude, + Selector: helper.ConvertMap(filter.Selector), } } diff --git a/pkg/config/resolver_test.go b/pkg/config/resolver_test.go index 0a9be6ee..1b8492cb 100644 --- a/pkg/config/resolver_test.go +++ b/pkg/config/resolver_test.go @@ -4,166 +4,208 @@ import ( "context" "testing" + "github.com/stretchr/testify/assert" "k8s.io/client-go/rest" "github.com/kyverno/policy-reporter/pkg/config" "github.com/kyverno/policy-reporter/pkg/database" "github.com/kyverno/policy-reporter/pkg/report" + "github.com/kyverno/policy-reporter/pkg/target" ) -var testConfig = &config.Config{ - Loki: &config.Loki{ - Host: "http://localhost:3100", - TargetBaseOptions: config.TargetBaseOptions{ - SkipExisting: true, - MinimumPriority: "debug", - CustomFields: map[string]string{"field": "value"}, +var targets = target.Targets{ + Loki: &target.Config[target.LokiOptions]{ + Config: &target.LokiOptions{ + HostOptions: target.HostOptions{ + Host: "http://localhost:3100", + SkipTLS: true, + }, }, - SkipTLS: true, - Channels: []*config.Loki{ + SkipExisting: true, + MinimumSeverity: "debug", + CustomFields: map[string]string{"field": "value"}, + Channels: []*target.Config[target.LokiOptions]{ { - TargetBaseOptions: config.TargetBaseOptions{ - CustomFields: map[string]string{"label2": "value2"}, + CustomFields: map[string]string{"label2": "value2"}, + }, + }, + }, + Elasticsearch: &target.Config[target.ElasticsearchOptions]{ + Config: &target.ElasticsearchOptions{ + HostOptions: target.HostOptions{ + Host: "http://localhost:9200", + SkipTLS: true, + }, + Index: "policy-reporter", + Rotation: "daily", + }, + SkipExisting: true, + MinimumSeverity: "debug", + CustomFields: map[string]string{"field": "value"}, + Channels: []*target.Config[target.ElasticsearchOptions]{{}}, + }, + Slack: &target.Config[target.SlackOptions]{ + Config: &target.SlackOptions{ + WebhookOptions: target.WebhookOptions{ + Webhook: "http://localhost:80", + SkipTLS: true, + }, + }, + SkipExisting: true, + MinimumSeverity: "debug", + CustomFields: map[string]string{"field": "value"}, + Channels: []*target.Config[target.SlackOptions]{{ + Config: &target.SlackOptions{ + WebhookOptions: target.WebhookOptions{ + Webhook: "http://localhost:9200", }, }, - }, - Headers: map[string]string{"X-Forward": "http://loki"}, - }, - Elasticsearch: &config.Elasticsearch{ - Host: "http://localhost:9200", - Index: "policy-reporter", - Rotation: "daily", - TargetBaseOptions: config.TargetBaseOptions{ - SkipExisting: true, - MinimumPriority: "debug", - CustomFields: map[string]string{"field": "value"}, - }, - SkipTLS: true, - Channels: []*config.Elasticsearch{{}}, - }, - Slack: &config.Slack{ - Webhook: "http://hook.slack:80", - TargetBaseOptions: config.TargetBaseOptions{ - SkipExisting: true, - MinimumPriority: "debug", - CustomFields: map[string]string{"field": "value"}, - }, - Channels: []*config.Slack{{ - Webhook: "http://localhost:9200", }, { - Channel: "general", - }}, - }, - Discord: &config.Discord{ - Webhook: "http://hook.discord:80", - TargetBaseOptions: config.TargetBaseOptions{ - SkipExisting: true, - MinimumPriority: "debug", - CustomFields: map[string]string{"field": "value"}, - }, - Channels: []*config.Discord{{ - Webhook: "http://localhost:9200", - }}, - }, - Teams: &config.Teams{ - TargetBaseOptions: config.TargetBaseOptions{ - SkipExisting: true, - MinimumPriority: "debug", - CustomFields: map[string]string{"field": "value"}, - }, - Webhook: "http://hook.teams:80", - SkipTLS: true, - Channels: []*config.Teams{{ - Webhook: "http://localhost:9200", - }}, - }, - UI: &config.UI{ - TargetBaseOptions: config.TargetBaseOptions{ - SkipExisting: true, - MinimumPriority: "debug", - }, - Host: "http://localhost:8080", - }, - Webhook: &config.Webhook{ - Host: "http://localhost:8080", - Headers: map[string]string{ - "X-Custom": "Header", - }, - TargetBaseOptions: config.TargetBaseOptions{ - SkipExisting: true, - MinimumPriority: "debug", - CustomFields: map[string]string{"field": "value"}, - }, - SkipTLS: true, - Channels: []*config.Webhook{{ - Host: "http://localhost:8081", - Headers: map[string]string{ - "X-Custom-2": "Header", + Config: &target.SlackOptions{ + Channel: "general", }, }}, }, - S3: &config.S3{ - TargetBaseOptions: config.TargetBaseOptions{ - SkipExisting: true, - MinimumPriority: "debug", - CustomFields: map[string]string{"field": "value"}, + Discord: &target.Config[target.WebhookOptions]{ + Config: &target.WebhookOptions{ + Webhook: "http://discord:80", + SkipTLS: true, }, - AWSConfig: config.AWSConfig{ - AccessKeyID: "AccessKey", - SecretAccessKey: "SecretAccessKey", - Endpoint: "https://storage.yandexcloud.net", - Region: "ru-central1", - }, - Bucket: "test", - BucketKeyEnabled: false, - KmsKeyID: "", - ServerSideEncryption: "", - PathStyle: true, - Prefix: "prefix", - Channels: []*config.S3{{}}, + SkipExisting: true, + MinimumSeverity: "debug", + CustomFields: map[string]string{"field": "value"}, + Channels: []*target.Config[target.WebhookOptions]{{ + Config: &target.WebhookOptions{ + Webhook: "http://localhost:9200", + }, + }}, }, - Kinesis: &config.Kinesis{ - TargetBaseOptions: config.TargetBaseOptions{ - SkipExisting: true, - MinimumPriority: "debug", - CustomFields: map[string]string{"field": "value"}, + Teams: &target.Config[target.WebhookOptions]{ + Config: &target.WebhookOptions{ + Webhook: "http://hook.teams:80", + SkipTLS: true, }, - AWSConfig: config.AWSConfig{ - AccessKeyID: "AccessKey", - SecretAccessKey: "SecretAccessKey", - Endpoint: "https://yds.serverless.yandexcloud.net", - Region: "ru-central1", - }, - StreamName: "policy-reporter", - Channels: []*config.Kinesis{{}}, + SkipExisting: true, + MinimumSeverity: "debug", + CustomFields: map[string]string{"field": "value"}, + Channels: []*target.Config[target.WebhookOptions]{{ + Config: &target.WebhookOptions{ + Webhook: "http://localhost:9200", + }, + }}, }, - SecurityHub: &config.SecurityHub{ - TargetBaseOptions: config.TargetBaseOptions{ - SkipExisting: true, - MinimumPriority: "debug", - CustomFields: map[string]string{"field": "value"}, + GoogleChat: &target.Config[target.WebhookOptions]{ + Config: &target.WebhookOptions{ + Webhook: "http://localhost:900/webhook", + SkipTLS: true, }, - AWSConfig: config.AWSConfig{ - AccessKeyID: "AccessKey", - SecretAccessKey: "SecretAccessKey", - Endpoint: "https://yds.serverless.yandexcloud.net", - Region: "ru-central1", - }, - AccountID: "AccountID", - Channels: []*config.SecurityHub{{}}, - DelayInSeconds: 2, + SkipExisting: true, + MinimumSeverity: "debug", + CustomFields: map[string]string{"field": "value"}, + Channels: []*target.Config[target.WebhookOptions]{{}}, }, - GCS: &config.GCS{ - TargetBaseOptions: config.TargetBaseOptions{ - SkipExisting: true, - MinimumPriority: "debug", - CustomFields: map[string]string{"field": "value"}, + Telegram: &target.Config[target.TelegramOptions]{ + Config: &target.TelegramOptions{ + WebhookOptions: target.WebhookOptions{ + Webhook: "http://localhost:80", + SkipTLS: true, + }, + Token: "XXX", + ChatID: "123456", }, - Credentials: `{"token": "token", "type": "authorized_user"}`, - Bucket: "test", - Prefix: "prefix", - Channels: []*config.GCS{{}}, + SkipExisting: true, + MinimumSeverity: "debug", + CustomFields: map[string]string{"field": "value"}, + Channels: []*target.Config[target.TelegramOptions]{{ + Config: &target.TelegramOptions{ + ChatID: "1234567", + }, + }}, }, + Webhook: &target.Config[target.WebhookOptions]{ + Config: &target.WebhookOptions{ + Webhook: "http://localhost:8080", + SkipTLS: true, + Headers: map[string]string{ + "X-Custom": "Header", + }, + }, + SkipExisting: true, + MinimumSeverity: "debug", + CustomFields: map[string]string{"field": "value"}, + Channels: []*target.Config[target.WebhookOptions]{{ + Config: &target.WebhookOptions{ + Webhook: "http://localhost:8081", + Headers: map[string]string{ + "X-Custom-2": "Header", + }, + }, + }}, + }, + S3: &target.Config[target.S3Options]{ + Config: &target.S3Options{ + AWSConfig: target.AWSConfig{ + AccessKeyID: "AccessKey", + SecretAccessKey: "SecretAccessKey", + Endpoint: "https://storage.yandexcloud.net", + Region: "ru-central1", + }, + Bucket: "test", + BucketKeyEnabled: false, + KmsKeyID: "", + ServerSideEncryption: "", + PathStyle: true, + Prefix: "prefix", + }, + SkipExisting: true, + MinimumSeverity: "debug", + CustomFields: map[string]string{"field": "value"}, + Channels: []*target.Config[target.S3Options]{{}}, + }, + Kinesis: &target.Config[target.KinesisOptions]{ + Config: &target.KinesisOptions{ + AWSConfig: target.AWSConfig{ + AccessKeyID: "AccessKey", + SecretAccessKey: "SecretAccessKey", + Endpoint: "https://storage.yandexcloud.net", + Region: "ru-central1", + }, + StreamName: "policy-reporter", + }, + SkipExisting: true, + MinimumSeverity: "debug", + CustomFields: map[string]string{"field": "value"}, + Channels: []*target.Config[target.KinesisOptions]{{}}, + }, + SecurityHub: &target.Config[target.SecurityHubOptions]{ + Config: &target.SecurityHubOptions{ + AWSConfig: target.AWSConfig{ + AccessKeyID: "AccessKey", + SecretAccessKey: "SecretAccessKey", + Endpoint: "https://storage.yandexcloud.net", + Region: "ru-central1", + }, + AccountID: "AccountId", + }, + SkipExisting: true, + MinimumSeverity: "debug", + CustomFields: map[string]string{"field": "value"}, + Channels: []*target.Config[target.SecurityHubOptions]{{}}, + }, + GCS: &target.Config[target.GCSOptions]{ + Config: &target.GCSOptions{ + Credentials: `{"token": "token", "type": "authorized_user"}`, + Bucket: "test", + Prefix: "prefix", + }, + SkipExisting: true, + MinimumSeverity: "debug", + CustomFields: map[string]string{"field": "value"}, + Channels: []*target.Config[target.GCSOptions]{{}}, + }, +} + +var testConfig = &config.Config{ Templates: config.Templates{ Dir: "../../templates", }, @@ -177,82 +219,78 @@ var testConfig = &config.Config{ Encryption: "ssl/tls", }, }, - Telegram: &config.Telegram{ - Token: "XXX", - ChatID: "123456", - Channels: []*config.Telegram{ - { - ChatID: "1234567", + Targets: targets, + Logging: config.Logging{ + Development: true, + }, + SourceConfig: []config.SourceConfig{ + { + Selector: config.SourceSelector{ + Source: "test", }, - }, - }, - GoogleChat: &config.GoogleChat{ - Webhook: "http://localhost:900/webhook", - Channels: []*config.GoogleChat{{}}, - }, - SourceConfig: map[string]config.SourceConfig{ - "test": { CustomID: config.CustomID{ Enabled: true, Fields: []string{"resource"}, }, }, - "default": {}, + { + Selector: config.SourceSelector{ + Source: "default", + }, + }, }, } func Test_ResolveTargets(t *testing.T) { resolver := config.NewResolver(testConfig, &rest.Config{}) - if count := len(resolver.TargetClients()); count != 26 { - t.Errorf("Expected 26 Clients, got %d", count) - } + assert.Equal(t, resolver.TargetClients().Length(), 25) } func Test_ResolveHasTargets(t *testing.T) { resolver := config.NewResolver(testConfig, &rest.Config{}) - if !resolver.HasTargets() { - t.Errorf("Expected 'true'") - } + assert.True(t, resolver.HasTargets()) } func Test_ResolveSkipExistingOnStartup(t *testing.T) { testConfig := &config.Config{ - Loki: &config.Loki{ - Host: "http://localhost:3100", - TargetBaseOptions: config.TargetBaseOptions{ + Targets: target.Targets{ + Loki: &target.Config[target.LokiOptions]{ + Config: &target.LokiOptions{ + HostOptions: target.HostOptions{ + Host: "http://localhost:3100", + }, + }, SkipExisting: true, - MinimumPriority: "debug", + MinimumSeverity: "debug", }, - }, - Elasticsearch: &config.Elasticsearch{ - Host: "http://localhost:9200", - TargetBaseOptions: config.TargetBaseOptions{ + Elasticsearch: &target.Config[target.ElasticsearchOptions]{ + Config: &target.ElasticsearchOptions{ + HostOptions: target.HostOptions{ + Host: "http://localhost:9200", + }, + }, SkipExisting: true, - MinimumPriority: "debug", + MinimumSeverity: "debug", }, }, } t.Run("Resolve false", func(t *testing.T) { - testConfig.Elasticsearch.SkipExisting = false + testConfig.Targets.Elasticsearch.SkipExisting = false resolver := config.NewResolver(testConfig, &rest.Config{}) - if resolver.SkipExistingOnStartup() == true { - t.Error("Expected SkipExistingOnStartup to be false if one Client has SkipExistingOnStartup false configured") - } + assert.False(t, resolver.SkipExistingOnStartup(), "Expected SkipExistingOnStartup to be false if one Client has SkipExistingOnStartup false configured") }) t.Run("Resolve true", func(t *testing.T) { - testConfig.Elasticsearch.SkipExisting = true + testConfig.Targets.Elasticsearch.SkipExisting = true resolver := config.NewResolver(testConfig, &rest.Config{}) - if resolver.SkipExistingOnStartup() == false { - t.Error("Expected SkipExistingOnStartup to be true if all Client has SkipExistingOnStartup true configured") - } + assert.True(t, resolver.SkipExistingOnStartup(), "Expected SkipExistingOnStartup to be true if all Client has SkipExistingOnStartup true configured") }) } @@ -260,28 +298,40 @@ func Test_ResolvePolicyClient(t *testing.T) { resolver := config.NewResolver(&config.Config{DBFile: "test.db"}, &rest.Config{}) client1, err := resolver.PolicyReportClient() - if err != nil { - t.Errorf("Unexpected Error: %s", err) - } + assert.Nil(t, err) client2, _ := resolver.PolicyReportClient() - if client1 != client2 { - t.Error("A second call resolver.PolicyReportClient() should return the cached first client") - } + + assert.Equal(t, client1, client2, "A second call resolver.PolicyReportClient() should return the cached first client") +} + +func Test_ResolveSecretInformer(t *testing.T) { + resolver := config.NewResolver(&config.Config{DBFile: "test.db"}, &rest.Config{}) + + informer, err := resolver.SecretInformer() + assert.Nil(t, err) + assert.NotNil(t, informer) +} + +func Test_ResolveSecretInformerWithInvalidK8sConfig(t *testing.T) { + k8sConfig := &rest.Config{} + k8sConfig.Host = "invalid/url" + + resolver := config.NewResolver(testConfig, k8sConfig) + + _, err := resolver.SecretInformer() + assert.NotNil(t, err, "Error: 'host must be a URL or a host:port pair' was expected") } func Test_ResolveLeaderElectionClient(t *testing.T) { resolver := config.NewResolver(&config.Config{DBFile: "test.db"}, &rest.Config{}) client1, err := resolver.LeaderElectionClient() - if err != nil { - t.Errorf("Unexpected Error: %s", err) - } + assert.Nil(t, err) client2, _ := resolver.LeaderElectionClient() - if client1 != client2 { - t.Error("A second call resolver.LeaderElectionClient() should return the cached first client") - } + + assert.Equal(t, client1, client2, "A second call resolver.LeaderElectionClient() should return the cached first client") } func Test_ResolvePolicyStore(t *testing.T) { @@ -289,24 +339,22 @@ func Test_ResolvePolicyStore(t *testing.T) { db := resolver.Database() defer db.Close() - store1, err := resolver.PolicyReportStore(db) - if err != nil { - t.Errorf("Unexpected Error: %s", err) - } + store1, err := resolver.Store(db) + assert.Nil(t, err) - store2, _ := resolver.PolicyReportStore(db) - if store1 != store2 { - t.Error("A second call resolver.PolicyReportClient() should return the cached first client") - } + store2, _ := resolver.Store(db) + assert.Equal(t, store1, store2, "A second call resolver.Store() should return the cached first client") } func Test_ResolveAPIServer(t *testing.T) { - resolver := config.NewResolver(&config.Config{}, &rest.Config{}) + resolver := config.NewResolver(&config.Config{ + API: config.API{ + BasicAuth: config.BasicAuth{Username: "user", Password: "password"}, + }, + }, &rest.Config{}) - server := resolver.APIServer(context.Background(), func() bool { return true }) - if server == nil { - t.Error("Error: Should return API Server") - } + server, _ := resolver.Server(context.Background(), nil) + assert.NotNil(t, server) } func Test_ResolveCache(t *testing.T) { @@ -314,14 +362,9 @@ func Test_ResolveCache(t *testing.T) { resolver := config.NewResolver(testConfig, &rest.Config{}) cache1 := resolver.ResultCache() - if cache1 == nil { - t.Error("Error: Should return ResultCache") - } + assert.NotNil(t, cache1) - cache2 := resolver.ResultCache() - if cache1 != cache2 { - t.Error("A second call resolver.ResultCache() should return the cached first cache") - } + assert.Equal(t, cache1, resolver.ResultCache(), "A second call resolver.ResultCache() should return the cached first client") }) t.Run("Redis", func(t *testing.T) { @@ -334,20 +377,14 @@ func Test_ResolveCache(t *testing.T) { resolver := config.NewResolver(redisConfig, &rest.Config{}) - cache1 := resolver.ResultCache() - if cache1 == nil { - t.Error("Error: Should return ResultCache") - } + assert.NotNil(t, resolver.ResultCache()) }) } func Test_ResolveReportFilter(t *testing.T) { resolver := config.NewResolver(testConfig, &rest.Config{}) - filter := resolver.ReportFilter() - if filter == nil { - t.Error("Error: Should return Filter") - } + assert.NotNil(t, resolver.ReportFilter()) } func Test_ResolveClientWithInvalidK8sConfig(t *testing.T) { @@ -357,9 +394,7 @@ func Test_ResolveClientWithInvalidK8sConfig(t *testing.T) { resolver := config.NewResolver(testConfig, k8sConfig) _, err := resolver.PolicyReportClient() - if err == nil { - t.Error("Error: 'host must be a URL or a host:port pair' was expected") - } + assert.NotNil(t, err, "Error: 'host must be a URL or a host:port pair' was expected") } func Test_ResolveLeaderElectionWithInvalidK8sConfig(t *testing.T) { @@ -369,18 +404,14 @@ func Test_ResolveLeaderElectionWithInvalidK8sConfig(t *testing.T) { resolver := config.NewResolver(testConfig, k8sConfig) _, err := resolver.LeaderElectionClient() - if err == nil { - t.Error("Error: 'host must be a URL or a host:port pair' was expected") - } + assert.NotNil(t, err, "Error: 'host must be a URL or a host:port pair' was expected") } func Test_ResolveCRDClient(t *testing.T) { resolver := config.NewResolver(testConfig, &rest.Config{}) _, err := resolver.CRDClient() - if err != nil { - t.Error("unexpected error") - } + assert.Nil(t, err) } func Test_ResolveCRDClientWithInvalidK8sConfig(t *testing.T) { @@ -390,18 +421,13 @@ func Test_ResolveCRDClientWithInvalidK8sConfig(t *testing.T) { resolver := config.NewResolver(testConfig, k8sConfig) _, err := resolver.CRDClient() - if err == nil { - t.Error("Error: 'host must be a URL or a host:port pair' was expected") - } + assert.NotNil(t, err, "Error: 'host must be a URL or a host:port pair' was expected") } func Test_ResolveSecretClient(t *testing.T) { resolver := config.NewResolver(testConfig, &rest.Config{}) - client := resolver.SecretClient() - if client == nil { - t.Error("unexpected error") - } + assert.NotNil(t, resolver.SecretClient()) } func Test_ResolveSecretCClientWithInvalidK8sConfig(t *testing.T) { @@ -411,9 +437,7 @@ func Test_ResolveSecretCClientWithInvalidK8sConfig(t *testing.T) { resolver := config.NewResolver(testConfig, k8sConfig) client := resolver.SecretClient() - if client != nil { - t.Error("Error: 'host must be a URL or a host:port pair' was expected") - } + assert.Nil(t, client, "Error: 'host must be a URL or a host:port pair' was expected") } func Test_RegisterStoreListener(t *testing.T) { @@ -421,9 +445,7 @@ func Test_RegisterStoreListener(t *testing.T) { resolver := config.NewResolver(testConfig, &rest.Config{}) resolver.RegisterStoreListener(context.Background(), report.NewPolicyReportStore()) - if len(resolver.EventPublisher().GetListener()) != 1 { - t.Error("Expected one Listener to be registered") - } + assert.Len(t, resolver.EventPublisher().GetListener(), 1, "Expected one Listener to be registered") }) } @@ -432,9 +454,7 @@ func Test_RegisterMetricsListener(t *testing.T) { resolver := config.NewResolver(testConfig, &rest.Config{}) resolver.RegisterMetricsListener() - if len(resolver.EventPublisher().GetListener()) != 1 { - t.Error("Expected one Listener to be registered") - } + assert.Len(t, resolver.EventPublisher().GetListener(), 1, "Expected one Listener to be registered") }) } @@ -443,18 +463,14 @@ func Test_RegisterSendResultListener(t *testing.T) { resolver := config.NewResolver(testConfig, &rest.Config{}) resolver.RegisterSendResultListener() - if len(resolver.EventPublisher().GetListener()) != 1 { - t.Error("Expected one Listener to be registered") - } + assert.Len(t, resolver.EventPublisher().GetListener(), 1, "Expected one Listener to be registered") }) t.Run("Register SendResultListener without Targets", func(t *testing.T) { resolver := config.NewResolver(&config.Config{}, &rest.Config{}) resolver.RegisterSendResultListener() - if len(resolver.EventPublisher().GetListener()) != 0 { - t.Error("Expected no Listener to be registered because no target exists") - } + assert.Len(t, resolver.EventPublisher().GetListener(), 0, "Expected no Listener to be registered because no target exists") }) } @@ -462,12 +478,9 @@ func Test_SummaryReportServices(t *testing.T) { t.Run("Generator", func(t *testing.T) { resolver := config.NewResolver(testConfig, &rest.Config{}) generator, err := resolver.SummaryGenerator() - if err != nil { - t.Errorf("Unexpected error: %s", err) - } - if generator == nil { - t.Error("Should return Generator Pointer") - } + + assert.Nil(t, err) + assert.NotNil(t, generator) }) t.Run("Generator.Error", func(t *testing.T) { k8sConfig := &rest.Config{} @@ -476,16 +489,12 @@ func Test_SummaryReportServices(t *testing.T) { resolver := config.NewResolver(testConfig, k8sConfig) _, err := resolver.SummaryGenerator() - if err == nil { - t.Error("Error: 'host must be a URL or a host:port pair' was expected") - } + assert.NotNil(t, err, "Error: 'host must be a URL or a host:port pair' was expected") }) t.Run("Reporter", func(t *testing.T) { resolver := config.NewResolver(testConfig, &rest.Config{}) - reporter := resolver.SummaryReporter() - if reporter == nil { - t.Error("Should return Reporter Pointer") - } + + assert.NotNil(t, resolver.SummaryReporter()) }) } @@ -493,12 +502,9 @@ func Test_ViolationReportServices(t *testing.T) { t.Run("Generator", func(t *testing.T) { resolver := config.NewResolver(testConfig, &rest.Config{}) generator, err := resolver.ViolationsGenerator() - if err != nil { - t.Errorf("Unexpected error: %s", err) - } - if generator == nil { - t.Error("Should return Generator Pointer") - } + + assert.Nil(t, err) + assert.NotNil(t, generator) }) t.Run("Generator.Error", func(t *testing.T) { k8sConfig := &rest.Config{} @@ -507,33 +513,25 @@ func Test_ViolationReportServices(t *testing.T) { resolver := config.NewResolver(testConfig, k8sConfig) _, err := resolver.ViolationsGenerator() - if err == nil { - t.Error("Error: 'host must be a URL or a host:port pair' was expected") - } + assert.NotNil(t, err, "Error: 'host must be a URL or a host:port pair' was expected") }) t.Run("Reporter", func(t *testing.T) { resolver := config.NewResolver(testConfig, &rest.Config{}) - reporter := resolver.ViolationsReporter() - if reporter == nil { - t.Error("Should return Reporter Pointer") - } + + assert.NotNil(t, resolver.ViolationsReporter()) }) } func Test_SMTP(t *testing.T) { t.Run("SMTP", func(t *testing.T) { resolver := config.NewResolver(testConfig, &rest.Config{}) - smtp := resolver.SMTPServer() - if smtp == nil { - t.Error("Should return SMTP Pointer") - } + + assert.NotNil(t, resolver.SMTPServer()) }) t.Run("EmailClient", func(t *testing.T) { resolver := config.NewResolver(testConfig, &rest.Config{}) - client := resolver.EmailClient() - if client == nil { - t.Error("Should return EmailClient Pointer") - } + + assert.NotNil(t, resolver.EmailClient()) }) } @@ -541,27 +539,31 @@ func Test_ResolveLogger(t *testing.T) { resolver := config.NewResolver(testConfig, &rest.Config{}) logger1, _ := resolver.Logger() - if logger1 == nil { - t.Error("Error: Should return Logger") - } + assert.NotNil(t, logger1) logger2, _ := resolver.Logger() - if logger1 != logger2 { - t.Error("A second call resolver.Mapper() should return the cached first cache") - } + assert.NotNil(t, logger2) + + assert.Equal(t, logger1, logger2, "A second call resolver.Logger() should return the cached first cache") } func Test_ResolveEnableLeaderElection(t *testing.T) { t.Run("general disabled", func(t *testing.T) { resolver := config.NewResolver(&config.Config{ LeaderElection: config.LeaderElection{Enabled: false}, - Loki: &config.Loki{Host: "localhost:3100"}, - Database: config.Database{Type: database.MySQL}, + Targets: target.Targets{ + Loki: &target.Config[target.LokiOptions]{ + Config: &target.LokiOptions{ + HostOptions: target.HostOptions{ + Host: "http://localhost:3100", + }, + }, + }, + }, + Database: config.Database{Type: database.MySQL}, }, &rest.Config{}) - if resolver.EnableLeaderElection() { - t.Error("leaderelection should be not enabled if its general disabled") - } + assert.False(t, resolver.EnableLeaderElection(), "leaderelection should be not enabled if its general disabled") }) t.Run("no pushes and SQLite Database", func(t *testing.T) { @@ -571,22 +573,26 @@ func Test_ResolveEnableLeaderElection(t *testing.T) { DBFile: "test.db", }, &rest.Config{}) - if resolver.EnableLeaderElection() { - t.Error("leaderelection should be not enabled if no pushes configured and SQLite is used") - } + assert.False(t, resolver.EnableLeaderElection(), "leaderelection should be not enabled if no pushes configured and SQLite is used") }) t.Run("enabled if pushes defined", func(t *testing.T) { resolver := config.NewResolver(&config.Config{ LeaderElection: config.LeaderElection{Enabled: true}, Database: config.Database{Type: database.SQLite}, - Loki: &config.Loki{Host: "localhost:3100"}, - DBFile: "test.db", + Targets: target.Targets{ + Loki: &target.Config[target.LokiOptions]{ + Config: &target.LokiOptions{ + HostOptions: target.HostOptions{ + Host: "http://localhost:3100", + }, + }, + }, + }, + DBFile: "test.db", }, &rest.Config{}) - if !resolver.EnableLeaderElection() { - t.Error("leaderelection should be enabled if general enabled and targets configured") - } + assert.True(t, resolver.EnableLeaderElection(), "leaderelection should be enabled if general enabled and targets configured") }) } @@ -594,7 +600,14 @@ func Test_ResolveCustomIDGenerators(t *testing.T) { resolver := config.NewResolver(testConfig, nil) generators := resolver.CustomIDGenerators() - if len(generators) != 1 { - t.Error("only enabled custom id config should be mapped") - } + assert.Len(t, generators, 1, "only enabled custom id config should be mapped") +} + +func Test_ResolveTargetCollection(t *testing.T) { + resolver := config.NewResolver(testConfig, &rest.Config{}) + + collection := resolver.TargetClients() + assert.NotNil(t, collection) + + assert.Equal(t, collection, resolver.TargetClients(), "A second call resolver.TargetClients() should return the cached first cache") } diff --git a/pkg/config/target_factory.go b/pkg/config/target_factory.go deleted file mode 100644 index 7e5701f3..00000000 --- a/pkg/config/target_factory.go +++ /dev/null @@ -1,984 +0,0 @@ -package config - -import ( - "context" - "encoding/json" - "fmt" - "os" - "strings" - "time" - - _ "github.com/mattn/go-sqlite3" - "go.uber.org/zap" - - "github.com/kyverno/policy-reporter/pkg/helper" - "github.com/kyverno/policy-reporter/pkg/kubernetes/secrets" - "github.com/kyverno/policy-reporter/pkg/report" - "github.com/kyverno/policy-reporter/pkg/target" - "github.com/kyverno/policy-reporter/pkg/target/discord" - "github.com/kyverno/policy-reporter/pkg/target/elasticsearch" - "github.com/kyverno/policy-reporter/pkg/target/gcs" - "github.com/kyverno/policy-reporter/pkg/target/googlechat" - "github.com/kyverno/policy-reporter/pkg/target/http" - "github.com/kyverno/policy-reporter/pkg/target/kinesis" - "github.com/kyverno/policy-reporter/pkg/target/loki" - "github.com/kyverno/policy-reporter/pkg/target/s3" - "github.com/kyverno/policy-reporter/pkg/target/securityhub" - "github.com/kyverno/policy-reporter/pkg/target/slack" - "github.com/kyverno/policy-reporter/pkg/target/teams" - "github.com/kyverno/policy-reporter/pkg/target/telegram" - "github.com/kyverno/policy-reporter/pkg/target/ui" - "github.com/kyverno/policy-reporter/pkg/target/webhook" -) - -// TargetFactory manages target creation -type TargetFactory struct { - secretClient secrets.Client -} - -// LokiClients resolver method -func (f *TargetFactory) LokiClients(config *Loki) []target.Client { - clients := make([]target.Client, 0) - if config == nil { - return clients - } - - setFallback(&config.Name, "Loki") - setFallback(&config.Path, "/api/prom/push") - - if loki := f.createLokiClient(config, &Loki{}); loki != nil { - clients = append(clients, loki) - } - for i, channel := range config.Channels { - setFallback(&config.Name, fmt.Sprintf("Loki Channel %d", i+1)) - - if loki := f.createLokiClient(channel, config); loki != nil { - clients = append(clients, loki) - } - } - - return clients -} - -// ElasticsearchClients resolver method -func (f *TargetFactory) ElasticsearchClients(config *Elasticsearch) []target.Client { - clients := make([]target.Client, 0) - if config == nil { - return clients - } - - setFallback(&config.Name, "Elasticsearch") - - if es := f.createElasticsearchClient(config, &Elasticsearch{}); es != nil { - clients = append(clients, es) - } - - for i, channel := range config.Channels { - setFallback(&config.Name, fmt.Sprintf("Elasticsearch Channel %d", i+1)) - - if es := f.createElasticsearchClient(channel, config); es != nil { - clients = append(clients, es) - } - } - - return clients -} - -// SlackClients resolver method -func (f *TargetFactory) SlackClients(config *Slack) []target.Client { - clients := make([]target.Client, 0) - if config == nil { - return clients - } - - setFallback(&config.Name, "Slack") - - if es := f.createSlackClient(config, &Slack{}); es != nil { - clients = append(clients, es) - } - for i, channel := range config.Channels { - setFallback(&config.Name, fmt.Sprintf("Slack Channel %d", i+1)) - - if es := f.createSlackClient(channel, config); es != nil { - clients = append(clients, es) - } - } - - return clients -} - -// DiscordClients resolver method -func (f *TargetFactory) DiscordClients(config *Discord) []target.Client { - clients := make([]target.Client, 0) - if config == nil { - return clients - } - - setFallback(&config.Name, "Discord") - - if es := f.createDiscordClient(config, &Discord{}); es != nil { - clients = append(clients, es) - } - for i, channel := range config.Channels { - setFallback(&config.Name, fmt.Sprintf("Discord Channel %d", i+1)) - - if es := f.createDiscordClient(channel, config); es != nil { - clients = append(clients, es) - } - } - - return clients -} - -// TeamsClients resolver method -func (f *TargetFactory) TeamsClients(config *Teams) []target.Client { - clients := make([]target.Client, 0) - if config == nil { - return clients - } - - setFallback(&config.Name, "Teams") - - if es := f.createTeamsClient(config, &Teams{}); es != nil { - clients = append(clients, es) - } - for i, channel := range config.Channels { - setFallback(&config.Name, fmt.Sprintf("Teams Channel %d", i+1)) - - if es := f.createTeamsClient(channel, config); es != nil { - clients = append(clients, es) - } - } - - return clients -} - -// WebhookClients resolver method -func (f *TargetFactory) WebhookClients(config *Webhook) []target.Client { - clients := make([]target.Client, 0) - if config == nil { - return clients - } - - setFallback(&config.Name, "Webhook") - - if es := f.createWebhookClient(config, &Webhook{}); es != nil { - clients = append(clients, es) - } - for i, channel := range config.Channels { - setFallback(&config.Name, fmt.Sprintf("Webhook Channel %d", i+1)) - - if es := f.createWebhookClient(channel, config); es != nil { - clients = append(clients, es) - } - } - - return clients -} - -// UIClient resolver method -func (f *TargetFactory) UIClient(config *UI) target.Client { - if config == nil || config.Host == "" { - return nil - } - - setFallback(&config.Name, "UI") - - zap.L().Info("UI configured") - - return ui.NewClient(ui.Options{ - ClientOptions: config.ClientOptions(), - Host: config.Host, - HTTPClient: http.NewClient(config.Certificate, config.SkipTLS), - }) -} - -// S3Clients resolver method -func (f *TargetFactory) S3Clients(config *S3) []target.Client { - clients := make([]target.Client, 0) - if config == nil { - return clients - } - - setFallback(&config.Name, "S3") - - if es := f.createS3Client(config, &S3{}); es != nil { - clients = append(clients, es) - } - for i, channel := range config.Channels { - setFallback(&config.Name, fmt.Sprintf("S3 Channel %d", i+1)) - - if es := f.createS3Client(channel, config); es != nil { - clients = append(clients, es) - } - } - - return clients -} - -// KinesisClients resolver method -func (f *TargetFactory) KinesisClients(config *Kinesis) []target.Client { - clients := make([]target.Client, 0) - if config == nil { - return clients - } - - setFallback(&config.Name, "Kinesis") - - if es := f.createKinesisClient(config, &Kinesis{}); es != nil { - clients = append(clients, es) - } - for i, channel := range config.Channels { - setFallback(&config.Name, fmt.Sprintf("Kinesis Channel %d", i+1)) - - if es := f.createKinesisClient(channel, config); es != nil { - clients = append(clients, es) - } - } - - return clients -} - -// SecurityHub resolver method -func (f *TargetFactory) SecurityHubs(config *SecurityHub) []target.Client { - clients := make([]target.Client, 0) - if config == nil { - return clients - } - - setFallback(&config.Name, "SecurityHub") - - if es := f.createSecurityHub(config, &SecurityHub{}); es != nil { - clients = append(clients, es) - } - for i, channel := range config.Channels { - setFallback(&config.Name, fmt.Sprintf("SecurityHub Channel %d", i+1)) - - if es := f.createSecurityHub(channel, config); es != nil { - clients = append(clients, es) - } - } - - return clients -} - -// GCSClients resolver method -func (f *TargetFactory) GCSClients(config *GCS) []target.Client { - clients := make([]target.Client, 0) - if config == nil { - return clients - } - - setFallback(&config.Name, "GoogleCloudStorage") - - if es := f.createGCSClient(config, &GCS{}); es != nil { - clients = append(clients, es) - } - for i, channel := range config.Channels { - setFallback(&config.Name, fmt.Sprintf("GCS Channel %d", i+1)) - - if es := f.createGCSClient(channel, config); es != nil { - clients = append(clients, es) - } - } - - return clients -} - -// TelegramClients resolver method -func (f *TargetFactory) TelegramClients(config *Telegram) []target.Client { - clients := make([]target.Client, 0) - if config == nil { - return clients - } - - setFallback(&config.Name, "Telegram") - - if es := f.createTelegramClient(config, &Telegram{}); es != nil { - clients = append(clients, es) - } - for i, channel := range config.Channels { - setFallback(&config.Name, fmt.Sprintf("Telegram Channel %d", i+1)) - - if es := f.createTelegramClient(channel, config); es != nil { - clients = append(clients, es) - } - } - - return clients -} - -// GoogleChatClients resolver method -func (f *TargetFactory) GoogleChatClients(config *GoogleChat) []target.Client { - clients := make([]target.Client, 0) - if config == nil { - return clients - } - - setFallback(&config.Name, "GoogleChat") - - if es := f.createGoogleChatClient(config, &GoogleChat{}); es != nil { - clients = append(clients, es) - } - for i, channel := range config.Channels { - setFallback(&config.Name, fmt.Sprintf("GoogleChat Channel %d", i+1)) - - if es := f.createGoogleChatClient(channel, config); es != nil { - clients = append(clients, es) - } - } - - return clients -} - -func (f *TargetFactory) createSlackClient(config, parent *Slack) target.Client { - if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { - f.mapSecretValues(config, config.SecretRef, config.MountedSecret) - } - - if config.Webhook == "" && config.Channel == "" { - return nil - } - - setFallback(&config.Webhook, parent.Webhook) - - if config.Webhook == "" { - return nil - } - - config.MapBaseParent(parent.TargetBaseOptions) - - zap.S().Infof("%s configured", config.Name) - - return slack.NewClient(slack.Options{ - ClientOptions: config.ClientOptions(), - Webhook: config.Webhook, - Channel: config.Channel, - CustomFields: config.CustomFields, - HTTPClient: http.NewClient("", false), - }) -} - -func (f *TargetFactory) createLokiClient(config, parent *Loki) target.Client { - if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { - f.mapSecretValues(config, config.SecretRef, config.MountedSecret) - } - - if config.Host == "" && parent.Host == "" { - return nil - } - - setFallback(&config.Host, parent.Host) - setFallback(&config.Certificate, parent.Certificate) - setFallback(&config.Path, parent.Path) - setBool(&config.SkipTLS, parent.SkipTLS) - setFallback(&config.Username, parent.Username) - setFallback(&config.Password, parent.Password) - - config.MapBaseParent(parent.TargetBaseOptions) - - zap.S().Infof("%s configured", config.Name) - - if config.CustomFields == nil { - config.CustomFields = make(map[string]string) - } - - if config.CustomLabels != nil { - for k, v := range config.CustomLabels { - config.CustomFields[k] = v - } - } - - return loki.NewClient(loki.Options{ - ClientOptions: config.ClientOptions(), - Host: config.Host + config.Path, - CustomLabels: config.CustomFields, - HTTPClient: http.NewClient(config.Certificate, config.SkipTLS), - Username: config.Username, - Password: config.Password, - Headers: config.Headers, - }) -} - -func (f *TargetFactory) createElasticsearchClient(config, parent *Elasticsearch) target.Client { - if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { - f.mapSecretValues(config, config.SecretRef, config.MountedSecret) - } - - if config.Host == "" && parent.Host == "" { - return nil - } - - setFallback(&config.Host, parent.Host) - setFallback(&config.Certificate, parent.Certificate) - setBool(&config.SkipTLS, parent.SkipTLS) - setFallback(&config.Username, parent.Username) - setFallback(&config.Password, parent.Password) - setFallback(&config.APIKey, parent.APIKey) - setFallback(&config.Index, parent.Index, "policy-reporter") - setFallback(&config.Rotation, parent.Rotation, elasticsearch.Daily) - setBool(&config.TypelessAPI, parent.TypelessAPI) - - config.MapBaseParent(parent.TargetBaseOptions) - - zap.S().Infof("%s configured", config.Name) - - return elasticsearch.NewClient(elasticsearch.Options{ - ClientOptions: config.ClientOptions(), - Host: config.Host, - Username: config.Username, - Password: config.Password, - ApiKey: config.APIKey, - Rotation: config.Rotation, - Index: config.Index, - CustomFields: config.CustomFields, - HTTPClient: http.NewClient(config.Certificate, config.SkipTLS), - TypelessApi: config.TypelessAPI, - }) -} - -func (f *TargetFactory) createDiscordClient(config, parent *Discord) target.Client { - if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { - f.mapSecretValues(config, config.SecretRef, config.MountedSecret) - } - - if config.Webhook == "" { - return nil - } - - config.MapBaseParent(parent.TargetBaseOptions) - - zap.S().Infof("%s configured", config.Name) - - return discord.NewClient(discord.Options{ - ClientOptions: config.ClientOptions(), - Webhook: config.Webhook, - CustomFields: config.CustomFields, - HTTPClient: http.NewClient("", false), - }) -} - -func (f *TargetFactory) createTeamsClient(config, parent *Teams) target.Client { - if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { - f.mapSecretValues(config, config.SecretRef, config.MountedSecret) - } - - if config.Webhook == "" { - return nil - } - - setFallback(&config.Certificate, parent.Certificate) - setBool(&config.SkipTLS, parent.SkipTLS) - - config.MapBaseParent(parent.TargetBaseOptions) - - zap.S().Infof("%s configured", config.Name) - - return teams.NewClient(teams.Options{ - ClientOptions: config.ClientOptions(), - Webhook: config.Webhook, - CustomFields: config.CustomFields, - HTTPClient: http.NewClient(config.Certificate, config.SkipTLS), - }) -} - -func (f *TargetFactory) createWebhookClient(config, parent *Webhook) target.Client { - if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { - f.mapSecretValues(config, config.SecretRef, config.MountedSecret) - } - - if config.Host == "" { - return nil - } - - setFallback(&config.Certificate, parent.Certificate) - setBool(&config.SkipTLS, parent.SkipTLS) - config.MapBaseParent(parent.TargetBaseOptions) - - if len(parent.Headers) > 0 { - headers := map[string]string{} - for header, value := range parent.Headers { - headers[header] = value - } - for header, value := range config.Headers { - headers[header] = value - } - - config.Headers = headers - } - - zap.S().Infof("%s configured", config.Name) - - return webhook.NewClient(webhook.Options{ - ClientOptions: config.ClientOptions(), - Host: config.Host, - Headers: config.Headers, - CustomFields: config.CustomFields, - HTTPClient: http.NewClient(config.Certificate, config.SkipTLS), - }) -} - -func (f *TargetFactory) createTelegramClient(config, parent *Telegram) target.Client { - if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { - f.mapSecretValues(config, config.SecretRef, config.MountedSecret) - } - - setFallback(&config.Token, parent.Token) - - if config.ChatID == "" || config.Token == "" { - return nil - } - - setFallback(&config.Host, parent.Host) - setFallback(&config.Certificate, parent.Certificate) - setBool(&config.SkipTLS, parent.SkipTLS) - - config.MapBaseParent(parent.TargetBaseOptions) - - if len(parent.Headers) > 0 { - headers := map[string]string{} - for header, value := range parent.Headers { - headers[header] = value - } - for header, value := range config.Headers { - headers[header] = value - } - - config.Headers = headers - } - - host := "https://api.telegram.org" - if config.Host != "" { - host = strings.TrimSuffix(config.Host, "/") - } - - zap.S().Infof("%s configured", config.Name) - - return telegram.NewClient(telegram.Options{ - ClientOptions: config.ClientOptions(), - Host: fmt.Sprintf("%s/bot%s/sendMessage", host, config.Token), - ChatID: config.ChatID, - Headers: config.Headers, - CustomFields: config.CustomFields, - HTTPClient: http.NewClient(config.Certificate, config.SkipTLS), - }) -} - -func (f *TargetFactory) createGoogleChatClient(config, parent *GoogleChat) target.Client { - if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { - f.mapSecretValues(config, config.SecretRef, config.MountedSecret) - } - - setFallback(&config.Webhook, parent.Webhook) - - if config.Webhook == "" { - return nil - } - - setFallback(&config.Certificate, parent.Certificate) - setBool(&config.SkipTLS, parent.SkipTLS) - config.MapBaseParent(parent.TargetBaseOptions) - - if len(parent.Headers) > 0 { - headers := map[string]string{} - for header, value := range parent.Headers { - headers[header] = value - } - for header, value := range config.Headers { - headers[header] = value - } - - config.Headers = headers - } - - zap.S().Infof("%s configured", config.Name) - - return googlechat.NewClient(googlechat.Options{ - ClientOptions: config.ClientOptions(), - Webhook: config.Webhook, - Headers: config.Headers, - CustomFields: config.CustomFields, - HTTPClient: http.NewClient(config.Certificate, config.SkipTLS), - }) -} - -func (f *TargetFactory) createS3Client(config, parent *S3) target.Client { - if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { - f.mapSecretValues(config, config.SecretRef, config.MountedSecret) - } - - config.MapAWSParent(parent.AWSConfig) - if config.Endpoint == "" && !hasAWSIdentity() { - return nil - } - - setFallback(&config.Bucket, parent.Bucket) - if config.Bucket == "" { - return nil - } - - sugar := zap.S() - - if err := checkAWSConfig(config.Name, config.AWSConfig, parent.AWSConfig); err != nil { - sugar.Error(err) - - return nil - } - setFallback(&config.Region, os.Getenv("AWS_REGION")) - setFallback(&config.Prefix, parent.Prefix, "policy-reporter") - setFallback(&config.KmsKeyID, parent.KmsKeyID) - setFallback(&config.ServerSideEncryption, parent.ServerSideEncryption) - setBool(&config.BucketKeyEnabled, parent.BucketKeyEnabled) - - config.MapBaseParent(parent.TargetBaseOptions) - - s3Client := helper.NewS3Client( - config.AccessKeyID, - config.SecretAccessKey, - config.Region, - config.Endpoint, - config.Bucket, - config.PathStyle, - helper.WithKMS(config.BucketKeyEnabled, &config.KmsKeyID, &config.ServerSideEncryption), - ) - - sugar.Infof("%s configured", config.Name) - - return s3.NewClient(s3.Options{ - ClientOptions: config.ClientOptions(), - S3: s3Client, - CustomFields: config.CustomFields, - Prefix: config.Prefix, - }) -} - -func (f *TargetFactory) createKinesisClient(config, parent *Kinesis) target.Client { - if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { - f.mapSecretValues(config, config.SecretRef, config.MountedSecret) - } - - config.MapAWSParent(parent.AWSConfig) - if config.Endpoint == "" { - return nil - } - - sugar := zap.S() - if err := checkAWSConfig(config.Name, config.AWSConfig, parent.AWSConfig); err != nil { - sugar.Error(err) - - return nil - } - - setFallback(&config.StreamName, parent.StreamName) - if config.StreamName == "" { - sugar.Errorf("%s.StreamName has not been declared", config.Name) - return nil - } - - config.MapBaseParent(parent.TargetBaseOptions) - - kinesisClient := helper.NewKinesisClient( - config.AccessKeyID, - config.SecretAccessKey, - config.Region, - config.Endpoint, - config.StreamName, - ) - - sugar.Infof("%s configured", config.Name) - - return kinesis.NewClient(kinesis.Options{ - ClientOptions: config.ClientOptions(), - CustomFields: config.CustomFields, - Kinesis: kinesisClient, - }) -} - -func (f *TargetFactory) createSecurityHub(config, parent *SecurityHub) target.Client { - if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { - f.mapSecretValues(config, config.SecretRef, config.MountedSecret) - } - - setFallback(&config.AccountID, parent.AccountID) - if !hasAWSIdentity() && config.AccountID == "" { - return nil - } - - sugar := zap.S() - if err := checkAWSConfig(config.Name, config.AWSConfig, parent.AWSConfig); err != nil { - sugar.Error(err) - - return nil - } - - config.MapAWSParent(parent.AWSConfig) - config.MapBaseParent(parent.TargetBaseOptions) - - client := helper.NewHubClient( - config.AccessKeyID, - config.SecretAccessKey, - config.Region, - config.Endpoint, - ) - - sugar.Infof("%s configured", config.Name) - - setFallback(&config.ProductName, parent.ProductName, "Policy Reporter") - setFallback(&config.CompanyName, parent.CompanyName, "Kyverno") - setInt(&config.DelayInSeconds, parent.DelayInSeconds) - - return securityhub.NewClient(securityhub.Options{ - ClientOptions: config.ClientOptions(), - CustomFields: config.CustomFields, - Client: client, - AccountID: config.AccountID, - Region: config.Region, - ProductName: config.ProductName, - CompanyName: config.CompanyName, - Delay: time.Duration(config.DelayInSeconds) * time.Second, - }) -} - -func (f *TargetFactory) createGCSClient(config, parent *GCS) target.Client { - if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { - f.mapSecretValues(config, config.SecretRef, config.MountedSecret) - } - - setFallback(&config.Bucket, parent.Bucket) - if config.Bucket == "" { - return nil - } - - sugar := zap.S() - - setFallback(&config.Credentials, parent.Credentials) - setFallback(&config.Prefix, parent.Prefix, "policy-reporter") - - config.MapBaseParent(parent.TargetBaseOptions) - - gcsClient := helper.NewGCSClient( - context.Background(), - config.Credentials, - config.Bucket, - ) - if gcsClient == nil { - return nil - } - - sugar.Infof("%s configured", config.Name) - - return gcs.NewClient(gcs.Options{ - ClientOptions: config.ClientOptions(), - Client: gcsClient, - CustomFields: config.CustomFields, - Prefix: config.Prefix, - }) -} - -func (f *TargetFactory) mapSecretValues(config any, ref, mountedSecret string) { - values := secrets.Values{} - - if ref != "" { - secretValues, err := f.secretClient.Get(context.Background(), ref) - values = secretValues - if err != nil { - zap.L().Warn("failed to get secret reference", zap.Error(err)) - return - } - } - - if mountedSecret != "" { - file, err := os.ReadFile(mountedSecret) - if err != nil { - zap.L().Warn("failed to get mounted secret", zap.Error(err)) - return - } - err = json.Unmarshal(file, &values) - if err != nil { - zap.L().Warn("failed to unmarshal mounted secret", zap.Error(err)) - return - } - } - - switch c := config.(type) { - case *Loki: - if values.Host != "" { - c.Host = values.Host - } - if values.Username != "" { - c.Username = values.Username - } - if values.Password != "" { - c.Password = values.Password - } - - case *Slack: - if values.Webhook != "" { - c.Webhook = values.Webhook - } - if values.Channel != "" { - c.Channel = values.Channel - } - - case *Discord: - if values.Webhook != "" { - c.Webhook = values.Webhook - } - - case *Teams: - if values.Webhook != "" { - c.Webhook = values.Webhook - } - - case *Elasticsearch: - if values.Host != "" { - c.Host = values.Host - } - if values.Username != "" { - c.Username = values.Username - } - if values.Password != "" { - c.Password = values.Password - } - if values.APIKey != "" { - c.APIKey = values.APIKey - } - if values.TypelessAPI != false { - c.TypelessAPI = values.TypelessAPI - } - - case *S3: - if values.AccessKeyID != "" { - c.AccessKeyID = values.AccessKeyID - } - if values.SecretAccessKey != "" { - c.SecretAccessKey = values.SecretAccessKey - } - if values.KmsKeyID != "" { - c.KmsKeyID = values.KmsKeyID - } - - case *Kinesis: - if values.AccessKeyID != "" { - c.AccessKeyID = values.AccessKeyID - } - if values.SecretAccessKey != "" { - c.SecretAccessKey = values.SecretAccessKey - } - - case *SecurityHub: - if values.AccessKeyID != "" { - c.AccessKeyID = values.AccessKeyID - } - if values.SecretAccessKey != "" { - c.SecretAccessKey = values.SecretAccessKey - } - if values.AccountID != "" { - c.AccountID = values.AccessKeyID - } - - case *GCS: - if values.Credentials != "" { - c.Credentials = values.Credentials - } - - case *Webhook: - if values.Host != "" { - c.Host = values.Host - } - if values.Token != "" { - if c.Headers == nil { - c.Headers = make(map[string]string) - } - - c.Headers["Authorization"] = values.Token - } - case *Telegram: - if values.Token != "" { - c.Token = values.Token - } - if values.Host != "" { - c.Host = values.Host - } - case *GoogleChat: - if values.Webhook != "" { - c.Webhook = values.Webhook - } - } -} - -func createResultFilter(filter TargetFilter, minimumPriority string, sources []string) *report.ResultFilter { - return target.NewResultFilter( - ToRuleSet(filter.Namespaces), - ToRuleSet(filter.Priorities), - ToRuleSet(filter.Policies), - minimumPriority, - sources, - ) -} - -func createReportFilter(filter TargetFilter) *report.ReportFilter { - return target.NewReportFilter( - ToRuleSet(filter.ReportLabels), - ) -} - -func NewTargetFactory(secretClient secrets.Client) *TargetFactory { - return &TargetFactory{secretClient: secretClient} -} - -func hasAWSIdentity() bool { - irsaARN := os.Getenv("AWS_ROLE_ARN") - irsaFile := os.Getenv("AWS_WEB_IDENTITY_TOKEN_FILE") - - podIdentityFile := os.Getenv("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE") - podIdentityURI := os.Getenv("AWS_CONTAINER_CREDENTIALS_FULL_URI") - - return (irsaARN != "" && irsaFile != "") || (podIdentityFile != "" && podIdentityURI != "") -} - -func checkAWSConfig(name string, config AWSConfig, parent AWSConfig) error { - noEnvConfig := !hasAWSIdentity() - - if noEnvConfig && (config.AccessKeyID == "" && parent.AccessKeyID == "") { - return fmt.Errorf("%s.AccessKeyID has not been declared", name) - } - - if noEnvConfig && (config.SecretAccessKey == "" && parent.SecretAccessKey == "") { - return fmt.Errorf("%s.SecretAccessKey has not been declared", name) - } - - if config.Region == "" && parent.Region == "" { - return fmt.Errorf("%s.Region has not been declared", name) - } - - return nil -} - -func setFallback(config *string, parents ...string) { - if *config == "" { - for _, p := range parents { - if p != "" { - *config = p - return - } - } - } -} - -func setInt(config *int, parents ...int) { - if *config == 0 { - for _, p := range parents { - if p > 0 { - *config = p - return - } - } - } -} - -func setBool(config *bool, parent bool) { - if *config == false { - *config = parent - } -} diff --git a/pkg/config/target_factory_test.go b/pkg/config/target_factory_test.go deleted file mode 100644 index d1db45ea..00000000 --- a/pkg/config/target_factory_test.go +++ /dev/null @@ -1,816 +0,0 @@ -package config_test - -import ( - "encoding/json" - "os" - "reflect" - "testing" - - "go.uber.org/zap" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes/fake" - v1 "k8s.io/client-go/kubernetes/typed/core/v1" - - "github.com/kyverno/policy-reporter/pkg/config" - "github.com/kyverno/policy-reporter/pkg/kubernetes/secrets" -) - -const ( - secretName = "secret-values" - mountedSecret = "/tmp/secrets-9999" -) - -func newFakeClient() v1.SecretInterface { - return fake.NewSimpleClientset(&corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: secretName, - Namespace: "default", - }, - Data: map[string][]byte{ - "host": []byte("http://localhost:9200"), - "username": []byte("username"), - "password": []byte("password"), - "apiKey": []byte("apiKey"), - "webhook": []byte("http://localhost:9200/webhook"), - "accessKeyID": []byte("accessKeyID"), - "secretAccessKey": []byte("secretAccessKey"), - "accountID": []byte("accountID"), - "kmsKeyId": []byte("kmsKeyId"), - "token": []byte("token"), - "credentials": []byte(`{"token": "token", "type": "authorized_user"}`), - "database": []byte("database"), - "dsn": []byte(""), - "typelessApi": []byte("true"), - }, - }).CoreV1().Secrets("default") -} - -func mountSecret() { - secretValues := secrets.Values{ - Host: "http://localhost:9200", - Webhook: "http://localhost:9200/webhook", - Username: "username", - Password: "password", - APIKey: "apiKey", - AccessKeyID: "accessKeyId", - SecretAccessKey: "secretAccessKey", - KmsKeyID: "kmsKeyId", - Token: "token", - Credentials: `{"token": "token", "type": "authorized_user"}`, - Database: "database", - DSN: "", - TypelessAPI: false, - } - file, _ := json.MarshalIndent(secretValues, "", " ") - _ = os.WriteFile(mountedSecret, file, 0o644) -} - -var logger = zap.NewNop() - -func Test_ResolveTarget(t *testing.T) { - factory := config.NewTargetFactory(nil) - - t.Run("Loki", func(t *testing.T) { - clients := factory.LokiClients(testConfig.Loki) - if len(clients) != 2 { - t.Errorf("Expected 2 Client, got %d clients", len(clients)) - } - }) - t.Run("Elasticsearch", func(t *testing.T) { - clients := factory.ElasticsearchClients(testConfig.Elasticsearch) - if len(clients) != 2 { - t.Errorf("Expected 2 Client, got %d clients", len(clients)) - } - }) - t.Run("Slack", func(t *testing.T) { - clients := factory.SlackClients(testConfig.Slack) - if len(clients) != 3 { - t.Error("Expected Client, got nil") - } - }) - t.Run("Discord", func(t *testing.T) { - clients := factory.DiscordClients(testConfig.Discord) - if len(clients) != 2 { - t.Error("Expected Client, got nil") - } - }) - t.Run("Teams", func(t *testing.T) { - clients := factory.TeamsClients(testConfig.Teams) - if len(clients) != 2 { - t.Errorf("Expected 2 Client, got %d clients", len(clients)) - } - }) - t.Run("Webhook", func(t *testing.T) { - clients := factory.WebhookClients(testConfig.Webhook) - if len(clients) != 2 { - t.Errorf("Expected 2 Client, got %d clients", len(clients)) - } - }) - t.Run("Telegram", func(t *testing.T) { - clients := factory.TelegramClients(testConfig.Telegram) - if len(clients) != 2 { - t.Errorf("Expected 2 Client, got %d clients", len(clients)) - } - }) - t.Run("GoogleChat", func(t *testing.T) { - clients := factory.GoogleChatClients(testConfig.GoogleChat) - if len(clients) != 2 { - t.Errorf("Expected 2 Client, got %d clients", len(clients)) - } - }) - t.Run("S3", func(t *testing.T) { - clients := factory.S3Clients(testConfig.S3) - if len(clients) != 2 { - t.Errorf("Expected 2 Client, got %d clients", len(clients)) - } - }) - t.Run("GCS", func(t *testing.T) { - clients := factory.GCSClients(testConfig.GCS) - if len(clients) != 2 { - t.Errorf("Expected 2 Client, got %d clients", len(clients)) - } - }) - t.Run("Kinesis", func(t *testing.T) { - clients := factory.KinesisClients(testConfig.Kinesis) - if len(clients) != 2 { - t.Errorf("Expected 2 Client, got %d clients", len(clients)) - } - }) - t.Run("SecurityHub", func(t *testing.T) { - clients := factory.SecurityHubs(testConfig.SecurityHub) - if len(clients) != 2 { - t.Errorf("Expected 2 Client, got %d clients", len(clients)) - } - }) -} - -func Test_ResolveTargetWithoutHost(t *testing.T) { - factory := config.NewTargetFactory(nil) - - t.Run("Loki", func(t *testing.T) { - if len(factory.LokiClients(&config.Loki{})) != 0 { - t.Error("Expected Client to be nil if no host is configured") - } - }) - t.Run("Elasticsearch", func(t *testing.T) { - if len(factory.ElasticsearchClients(&config.Elasticsearch{})) != 0 { - t.Error("Expected Client to be nil if no host is configured") - } - }) - t.Run("Slack", func(t *testing.T) { - if len(factory.SlackClients(&config.Slack{})) != 0 { - t.Error("Expected Client to be nil if no host is configured") - } - }) - t.Run("Discord", func(t *testing.T) { - if len(factory.DiscordClients(&config.Discord{})) != 0 { - t.Error("Expected Client to be nil if no host is configured") - } - }) - t.Run("Teams", func(t *testing.T) { - if len(factory.TeamsClients(&config.Teams{})) != 0 { - t.Error("Expected Client to be nil if no host is configured") - } - }) - t.Run("Webhook", func(t *testing.T) { - if len(factory.WebhookClients(&config.Webhook{})) != 0 { - t.Error("Expected Client to be nil if no host is configured") - } - }) - t.Run("Telegram", func(t *testing.T) { - if len(factory.TelegramClients(&config.Telegram{})) != 0 { - t.Error("Expected Client to be nil if no chatID is configured") - } - }) - t.Run("GoogleChat", func(t *testing.T) { - if len(factory.GoogleChatClients(&config.GoogleChat{})) != 0 { - t.Error("Expected Client to be nil if no webhook is configured") - } - }) - t.Run("S3.Endoint", func(t *testing.T) { - if len(factory.S3Clients(&config.S3{})) != 0 { - t.Error("Expected Client to be nil if no endpoint is configured") - } - }) - t.Run("S3.AccessKey", func(t *testing.T) { - if len(factory.S3Clients(&config.S3{AWSConfig: config.AWSConfig{Endpoint: "https://storage.yandexcloud.net"}})) != 0 { - t.Error("Expected Client to be nil if no accessKey is configured") - } - }) - t.Run("S3.SecretAccessKey", func(t *testing.T) { - if len(factory.S3Clients(&config.S3{AWSConfig: config.AWSConfig{Endpoint: "https://storage.yandexcloud.net", AccessKeyID: "access"}})) != 0 { - t.Error("Expected Client to be nil if no secretAccessKey is configured") - } - }) - t.Run("S3.Region", func(t *testing.T) { - if len(factory.S3Clients(&config.S3{AWSConfig: config.AWSConfig{Endpoint: "https://storage.yandexcloud.net", AccessKeyID: "access", SecretAccessKey: "secret"}})) != 0 { - t.Error("Expected Client to be nil if no region is configured") - } - }) - t.Run("S3.Bucket", func(t *testing.T) { - if len(factory.S3Clients(&config.S3{AWSConfig: config.AWSConfig{Endpoint: "https://storage.yandexcloud.net", AccessKeyID: "access", SecretAccessKey: "secret", Region: "ru-central1"}})) != 0 { - t.Error("Expected Client to be nil if no bucket is configured") - } - }) - t.Run("S3.SSE-S3", func(t *testing.T) { - if len(factory.S3Clients(&config.S3{AWSConfig: config.AWSConfig{Endpoint: "https://storage.yandexcloud.net", AccessKeyID: "access", SecretAccessKey: "secret", Region: "ru-central1"}, ServerSideEncryption: "AES256"})) != 0 { - t.Error("Expected Client to be nil if server side encryption is not configured") - } - }) - t.Run("S3.SSE-KMS", func(t *testing.T) { - if len(factory.S3Clients(&config.S3{AWSConfig: config.AWSConfig{Endpoint: "https://storage.yandexcloud.net", AccessKeyID: "access", SecretAccessKey: "secret", Region: "ru-central1"}, ServerSideEncryption: "aws:kms"})) != 0 { - t.Error("Expected Client to be nil if server side encryption is not configured") - } - }) - t.Run("S3.SSE-KMS-S3-KEY", func(t *testing.T) { - if len(factory.S3Clients(&config.S3{AWSConfig: config.AWSConfig{Endpoint: "https://storage.yandexcloud.net", AccessKeyID: "access", SecretAccessKey: "secret", Region: "ru-central1"}, BucketKeyEnabled: true, ServerSideEncryption: "aws:kms"})) != 0 { - t.Error("Expected Client to be nil if server side encryption is not configured") - } - }) - t.Run("S3.SSE-KMS-KEY-ID", func(t *testing.T) { - if len(factory.S3Clients(&config.S3{AWSConfig: config.AWSConfig{Endpoint: "https://storage.yandexcloud.net", AccessKeyID: "access", SecretAccessKey: "secret", Region: "ru-central1"}, ServerSideEncryption: "aws:kms", KmsKeyID: "kmsKeyId"})) != 0 { - t.Error("Expected Client to be nil if server side encryption is not configured") - } - }) - t.Run("Kinesis.Endpoint", func(t *testing.T) { - if len(factory.KinesisClients(&config.Kinesis{})) != 0 { - t.Error("Expected Client to be nil if no endpoint is configured") - } - }) - t.Run("Kinesis.AccessKey", func(t *testing.T) { - if len(factory.KinesisClients(&config.Kinesis{AWSConfig: config.AWSConfig{Endpoint: "https://yds.serverless.yandexcloud.net"}})) != 0 { - t.Error("Expected Client to be nil if no accessKey is configured") - } - }) - t.Run("Kinesis.SecretAccessKey", func(t *testing.T) { - if len(factory.KinesisClients(&config.Kinesis{AWSConfig: config.AWSConfig{Endpoint: "https://yds.serverless.yandexcloud.net", AccessKeyID: "access"}})) != 0 { - t.Error("Expected Client to be nil if no secretAccessKey is configured") - } - }) - t.Run("Kinesis.Region", func(t *testing.T) { - if len(factory.KinesisClients(&config.Kinesis{AWSConfig: config.AWSConfig{Endpoint: "https://yds.serverless.yandexcloud.net", AccessKeyID: "access", SecretAccessKey: "secret"}})) != 0 { - t.Error("Expected Client to be nil if no region is configured") - } - }) - t.Run("Kinesis.StreamName", func(t *testing.T) { - if len(factory.KinesisClients(&config.Kinesis{AWSConfig: config.AWSConfig{Endpoint: "https://yds.serverless.yandexcloud.net", AccessKeyID: "access", SecretAccessKey: "secret", Region: "ru-central1"}})) != 0 { - t.Error("Expected Client to be nil if no stream name is configured") - } - }) - t.Run("SecurityHub.AccountID", func(t *testing.T) { - if len(factory.SecurityHubs(&config.SecurityHub{})) != 0 { - t.Error("Expected Client to be nil if no accountID is configured") - } - }) - t.Run("SecurityHub.AccessKey", func(t *testing.T) { - if len(factory.SecurityHubs(&config.SecurityHub{AccountID: "accountID"})) != 0 { - t.Error("Expected Client to be nil if no accessKey is configured") - } - }) - t.Run("SecurityHub.SecretAccessKey", func(t *testing.T) { - if len(factory.SecurityHubs(&config.SecurityHub{AccountID: "accountID", AWSConfig: config.AWSConfig{AccessKeyID: "access"}})) != 0 { - t.Error("Expected Client to be nil if no secretAccessKey is configured") - } - }) - t.Run("SecurityHub.Region", func(t *testing.T) { - if len(factory.SecurityHubs(&config.SecurityHub{AccountID: "accountID", AWSConfig: config.AWSConfig{AccessKeyID: "access", SecretAccessKey: "secret"}})) != 0 { - t.Error("Expected Client to be nil if no region is configured") - } - }) - t.Run("GCS.Bucket", func(t *testing.T) { - if len(factory.GCSClients(&config.GCS{})) != 0 { - t.Error("Expected Client to be nil if no bucket is configured") - } - }) - t.Run("GCS.Credentials", func(t *testing.T) { - if len(factory.GCSClients(&config.GCS{Bucket: "policy-reporter", Credentials: "{}"})) != 0 { - t.Error("Expected Client to be nil if no accessKey is configured") - } - }) -} - -func Test_GetValuesFromSecret(t *testing.T) { - factory := config.NewTargetFactory(secrets.NewClient(newFakeClient())) - - t.Run("Get Loki values from Secret", func(t *testing.T) { - clients := factory.LokiClients(&config.Loki{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}}) - if len(clients) != 1 { - t.Fatal("Expected one client created") - } - - client := reflect.ValueOf(clients[0]).Elem() - - if v := client.FieldByName("host").String(); v != "http://localhost:9200/api/prom/push" { - t.Errorf("Expected host from secret, got %s", v) - } - - username := client.FieldByName("username").String() - if username != "username" { - t.Errorf("Expected username from secret, got %s", username) - } - - password := client.FieldByName("password").String() - if password != "password" { - t.Errorf("Expected password from secret, got %s", password) - } - }) - - t.Run("Get Elasticsearch values from Secret", func(t *testing.T) { - clients := factory.ElasticsearchClients(&config.Elasticsearch{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}}) - if len(clients) != 1 { - t.Fatal("Expected one client created") - } - - client := reflect.ValueOf(clients[0]).Elem() - - host := client.FieldByName("host").String() - if host != "http://localhost:9200" { - t.Errorf("Expected host from secret, got %s", host) - } - - username := client.FieldByName("username").String() - if username != "username" { - t.Errorf("Expected username from secret, got %s", username) - } - - rotation := client.FieldByName("rotation").String() - if rotation != "daily" { - t.Errorf("Expected rotation from secret, got %s", rotation) - } - - index := client.FieldByName("index").String() - if index != "policy-reporter" { - t.Errorf("Expected rotation from secret, got %s", index) - } - - password := client.FieldByName("password").String() - if password != "password" { - t.Errorf("Expected password from secret, got %s", password) - } - - apiKey := client.FieldByName("apiKey").String() - if apiKey != "apiKey" { - t.Errorf("Expected apiKey from secret, got %s", apiKey) - } - - typelessApi := client.FieldByName("typelessApi").Bool() - if typelessApi == false { - t.Errorf("Expected typelessApi true value from secret, got %t", typelessApi) - } - }) - - t.Run("Get Discord values from Secret", func(t *testing.T) { - clients := factory.DiscordClients(&config.Discord{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}}) - if len(clients) != 1 { - t.Error("Expected one client created") - } - - client := reflect.ValueOf(clients[0]).Elem() - - webhook := client.FieldByName("webhook").String() - if webhook != "http://localhost:9200/webhook" { - t.Errorf("Expected webhook from secret, got %s", webhook) - } - }) - - t.Run("Get MS Teams values from Secret", func(t *testing.T) { - clients := factory.TeamsClients(&config.Teams{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}}) - if len(clients) != 1 { - t.Error("Expected one client created") - } - - client := reflect.ValueOf(clients[0]).Elem() - - webhook := client.FieldByName("webhook").String() - if webhook != "http://localhost:9200/webhook" { - t.Errorf("Expected webhook from secret, got %s", webhook) - } - }) - - t.Run("Get Slack values from Secret", func(t *testing.T) { - clients := factory.SlackClients(&config.Slack{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}}) - if len(clients) != 1 { - t.Error("Expected one client created") - } - - client := reflect.ValueOf(clients[0]).Elem() - - webhook := client.FieldByName("webhook").String() - if webhook != "http://localhost:9200/webhook" { - t.Errorf("Expected webhook from secret, got %s", webhook) - } - }) - - t.Run("Get Webhook Authentication Token from Secret", func(t *testing.T) { - clients := factory.WebhookClients(&config.Webhook{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}}) - if len(clients) != 1 { - t.Error("Expected one client created") - } - - client := reflect.ValueOf(clients[0]).Elem() - - token := client.FieldByName("headers").MapIndex(reflect.ValueOf("Authorization")).String() - if token != "token" { - t.Errorf("Expected token from secret, got %s", token) - } - }) - - t.Run("Get Telegram Token from Secret", func(t *testing.T) { - clients := factory.TelegramClients(&config.Telegram{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}, ChatID: "1234"}) - if len(clients) != 1 { - t.Error("Expected one client created") - } - - client := reflect.ValueOf(clients[0]).Elem() - - host := client.FieldByName("host").String() - if host != "http://localhost:9200/bottoken/sendMessage" { - t.Errorf("Expected host with token from secret, got %s", host) - } - }) - t.Run("Get GoogleChat Webhook from Secret", func(t *testing.T) { - clients := factory.GoogleChatClients(&config.GoogleChat{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}}) - if len(clients) != 1 { - t.Error("Expected one client created") - } - - client := reflect.ValueOf(clients[0]).Elem() - - host := client.FieldByName("webhook").String() - if host != "http://localhost:9200/webhook" { - t.Errorf("Expected host with token from secret, got %s", host) - } - }) - - t.Run("Get S3 values from Secret", func(t *testing.T) { - clients := factory.S3Clients(&config.S3{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}, AWSConfig: config.AWSConfig{Endpoint: "endoint", Region: "region"}, Bucket: "bucket"}) - if len(clients) != 1 { - t.Error("Expected one client created") - } - }) - - t.Run("Get S3 values from Secret with KMS", func(t *testing.T) { - clients := factory.S3Clients(&config.S3{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}, AWSConfig: config.AWSConfig{Endpoint: "endoint", Region: "region"}, Bucket: "bucket", BucketKeyEnabled: true, ServerSideEncryption: "aws:kms"}) - if len(clients) != 1 { - t.Error("Expected one client created") - } - }) - - t.Run("Get Kinesis values from Secret", func(t *testing.T) { - clients := factory.KinesisClients(&config.Kinesis{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}, AWSConfig: config.AWSConfig{Endpoint: "endpoint", Region: "region"}, StreamName: "stream"}) - if len(clients) != 1 { - t.Error("Expected one client created") - } - }) - - t.Run("Get SecurityHub values from Secret", func(t *testing.T) { - clients := factory.SecurityHubs(&config.SecurityHub{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}, AWSConfig: config.AWSConfig{Endpoint: "endpoint", Region: "region"}}) - if len(clients) != 1 { - t.Error("Expected one client created") - } - }) - - t.Run("Get GCS values from Secret", func(t *testing.T) { - clients := factory.GCSClients(&config.GCS{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}, Bucket: "bucket"}) - if len(clients) != 1 { - t.Error("Expected one client created") - } - }) - - t.Run("Get none existing secret skips target", func(t *testing.T) { - clients := factory.LokiClients(&config.Loki{TargetBaseOptions: config.TargetBaseOptions{SecretRef: "no-exist"}}) - if len(clients) != 0 { - t.Error("Expected client are skipped") - } - }) - - t.Run("Get CustomFields from Slack", func(t *testing.T) { - clients := factory.SlackClients(&config.Slack{TargetBaseOptions: config.TargetBaseOptions{CustomFields: map[string]string{"field": "value"}}, Webhook: "http://localhost"}) - if len(clients) != 1 { - t.Error("Expected one client created") - } - - client := reflect.ValueOf(clients[0]).Elem() - - customFields := client.FieldByName("customFields").MapKeys() - if customFields[0].String() != "field" { - t.Errorf("Expected customFields are added") - } - }) - t.Run("Get CustomFields from Discord", func(t *testing.T) { - clients := factory.DiscordClients(&config.Discord{TargetBaseOptions: config.TargetBaseOptions{CustomFields: map[string]string{"field": "value"}}, Webhook: "http://localhost"}) - if len(clients) != 1 { - t.Error("Expected one client created") - } - - client := reflect.ValueOf(clients[0]).Elem() - - customFields := client.FieldByName("customFields").MapKeys() - if customFields[0].String() != "field" { - t.Errorf("Expected customFields are added") - } - }) - t.Run("Get CustomFields from MS Teams", func(t *testing.T) { - clients := factory.TeamsClients(&config.Teams{TargetBaseOptions: config.TargetBaseOptions{CustomFields: map[string]string{"field": "value"}}, Webhook: "http://localhost"}) - if len(clients) != 1 { - t.Error("Expected one client created") - } - - client := reflect.ValueOf(clients[0]).Elem() - - customFields := client.FieldByName("customFields").MapKeys() - if customFields[0].String() != "field" { - t.Errorf("Expected customFields are added") - } - }) - t.Run("Get CustomFields from Elasticsearch", func(t *testing.T) { - clients := factory.ElasticsearchClients(&config.Elasticsearch{TargetBaseOptions: config.TargetBaseOptions{CustomFields: map[string]string{"field": "value"}}, Host: "http://localhost"}) - if len(clients) != 1 { - t.Error("Expected one client created") - } - - client := reflect.ValueOf(clients[0]).Elem() - - customFields := client.FieldByName("customFields").MapKeys() - if customFields[0].String() != "field" { - t.Errorf("Expected customFields are added") - } - }) - t.Run("Get CustomFields from Webhook", func(t *testing.T) { - clients := factory.WebhookClients(&config.Webhook{TargetBaseOptions: config.TargetBaseOptions{CustomFields: map[string]string{"field": "value"}}, Host: "http://localhost"}) - if len(clients) != 1 { - t.Error("Expected one client created") - } - - client := reflect.ValueOf(clients[0]).Elem() - - customFields := client.FieldByName("customFields").MapKeys() - if customFields[0].String() != "field" { - t.Errorf("Expected customFields are added") - } - }) - t.Run("Get CustomFields from Telegram", func(t *testing.T) { - clients := factory.TelegramClients(&config.Telegram{TargetBaseOptions: config.TargetBaseOptions{CustomFields: map[string]string{"field": "value"}}, Token: "XXX", ChatID: "1234"}) - if len(clients) != 1 { - t.Error("Expected one client created") - } - - client := reflect.ValueOf(clients[0]).Elem() - - customFields := client.FieldByName("customFields").MapKeys() - if customFields[0].String() != "field" { - t.Errorf("Expected customFields are added") - } - }) - t.Run("Get CustomFields from GoogleChat", func(t *testing.T) { - clients := factory.GoogleChatClients(&config.GoogleChat{TargetBaseOptions: config.TargetBaseOptions{CustomFields: map[string]string{"field": "value"}}, Webhook: "http;//googlechat.webhook"}) - if len(clients) != 1 { - t.Error("Expected one client created") - } - - client := reflect.ValueOf(clients[0]).Elem() - - customFields := client.FieldByName("customFields").MapKeys() - if customFields[0].String() != "field" { - t.Errorf("Expected customFields are added") - } - }) - t.Run("Get CustomFields from Kinesis", func(t *testing.T) { - clients := factory.KinesisClients(testConfig.Kinesis) - if len(clients) < 1 { - t.Error("Expected one client created") - } - - client := reflect.ValueOf(clients[0]).Elem() - - customFields := client.FieldByName("customFields").MapKeys() - if customFields[0].String() != "field" { - t.Errorf("Expected customFields are added") - } - }) - t.Run("Get CustomFields from S3", func(t *testing.T) { - clients := factory.S3Clients(testConfig.S3) - if len(clients) < 1 { - t.Error("Expected one client created") - } - - client := reflect.ValueOf(clients[0]).Elem() - - customFields := client.FieldByName("customFields").MapKeys() - if customFields[0].String() != "field" { - t.Errorf("Expected customFields are added") - } - }) - t.Run("Get CustomLabels from Loki", func(t *testing.T) { - clients := factory.LokiClients(&config.Loki{ - CustomLabels: map[string]string{"label": "value"}, - Host: "http://localhost", - }) - if len(clients) < 1 { - t.Error("Expected one client created") - } - - client := reflect.ValueOf(clients[0]).Elem() - - customFields := client.FieldByName("customLabels").MapKeys() - if customFields[0].String() != "label" { - t.Errorf("Expected customLabels are added") - } - }) - t.Run("Get CustomFields from GCS", func(t *testing.T) { - clients := factory.GCSClients(testConfig.GCS) - if len(clients) < 1 { - t.Error("Expected one client created") - } - - client := reflect.ValueOf(clients[0]).Elem() - - customFields := client.FieldByName("customFields").MapKeys() - if customFields[0].String() != "field" { - t.Errorf("Expected customFields are added") - } - }) -} - -func Test_GetValuesFromMountedSecret(t *testing.T) { - factory := config.NewTargetFactory(nil) - mountSecret() - defer os.Remove(mountedSecret) - - t.Run("Get Loki values from MountedSecret", func(t *testing.T) { - clients := factory.LokiClients(&config.Loki{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}}) - if len(clients) != 1 { - t.Error("Expected one client created") - } - - client := reflect.ValueOf(clients[0]).Elem() - if v := client.FieldByName("host").String(); v != "http://localhost:9200/api/prom/push" { - t.Errorf("Expected host from mounted secret, got %s", v) - } - - username := client.FieldByName("username").String() - if username != "username" { - t.Errorf("Expected username from mounted secret, got %s", username) - } - - password := client.FieldByName("password").String() - if password != "password" { - t.Errorf("Expected password from mounted secret, got %s", password) - } - - }) - - t.Run("Get Elasticsearch values from MountedSecret", func(t *testing.T) { - clients := factory.ElasticsearchClients(&config.Elasticsearch{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}}) - if len(clients) != 1 { - t.Error("Expected one client created") - } - - client := reflect.ValueOf(clients[0]).Elem() - - host := client.FieldByName("host").String() - if host != "http://localhost:9200" { - t.Errorf("Expected host from mounted secret, got %s", host) - } - - username := client.FieldByName("username").String() - if username != "username" { - t.Errorf("Expected username from mounted secret, got %s", username) - } - - password := client.FieldByName("password").String() - if password != "password" { - t.Errorf("Expected password from mounted secret, got %s", password) - } - - apiKey := client.FieldByName("apiKey").String() - if apiKey != "apiKey" { - t.Errorf("Expected apiKey from secret, got %s", apiKey) - } - - typelessApi := client.FieldByName("typelessApi").Bool() - if typelessApi != false { - t.Errorf("Expected typelessApi false value from secret, got %t", typelessApi) - } - }) - - t.Run("Get Discord values from MountedSecret", func(t *testing.T) { - clients := factory.DiscordClients(&config.Discord{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}}) - if len(clients) != 1 { - t.Error("Expected one client created") - } - - client := reflect.ValueOf(clients[0]).Elem() - - webhook := client.FieldByName("webhook").String() - if webhook != "http://localhost:9200/webhook" { - t.Errorf("Expected webhook from mounted secret, got %s", webhook) - } - }) - - t.Run("Get MS Teams values from MountedSecret", func(t *testing.T) { - clients := factory.TeamsClients(&config.Teams{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}}) - if len(clients) != 1 { - t.Error("Expected one client created") - } - - client := reflect.ValueOf(clients[0]).Elem() - - webhook := client.FieldByName("webhook").String() - if webhook != "http://localhost:9200/webhook" { - t.Errorf("Expected webhook from mounted secret, got %s", webhook) - } - }) - - t.Run("Get Slack values from MountedSecret", func(t *testing.T) { - clients := factory.SlackClients(&config.Slack{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}}) - if len(clients) != 1 { - t.Error("Expected one client created") - } - - client := reflect.ValueOf(clients[0]).Elem() - - webhook := client.FieldByName("webhook").String() - if webhook != "http://localhost:9200/webhook" { - t.Errorf("Expected webhook from mounted secret, got %s", webhook) - } - }) - - t.Run("Get Webhook Authentication Token from MountedSecret", func(t *testing.T) { - clients := factory.WebhookClients(&config.Webhook{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}}) - if len(clients) != 1 { - t.Error("Expected one client created") - } - - client := reflect.ValueOf(clients[0]).Elem() - - token := client.FieldByName("headers").MapIndex(reflect.ValueOf("Authorization")).String() - if token != "token" { - t.Errorf("Expected token from mounted secret, got %s", token) - } - }) - - t.Run("Get Telegram Token from MountedSecret", func(t *testing.T) { - clients := factory.TelegramClients(&config.Telegram{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}, ChatID: "123"}) - if len(clients) != 1 { - t.Error("Expected one client created") - } - - client := reflect.ValueOf(clients[0]).Elem() - - token := client.FieldByName("host").String() - if token != "http://localhost:9200/bottoken/sendMessage" { - t.Errorf("Expected token from mounted secret, got %s", token) - } - }) - - t.Run("Get GoogleChat Webhook from MountedSecret", func(t *testing.T) { - clients := factory.GoogleChatClients(&config.GoogleChat{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}}) - if len(clients) != 1 { - t.Error("Expected one client created") - } - - client := reflect.ValueOf(clients[0]).Elem() - - token := client.FieldByName("webhook").String() - if token != "http://localhost:9200/webhook" { - t.Errorf("Expected token from mounted secret, got %s", token) - } - }) - - t.Run("Get S3 values from MountedSecret", func(t *testing.T) { - clients := factory.S3Clients(&config.S3{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}, AWSConfig: config.AWSConfig{Endpoint: "endpoint", Region: "region"}, Bucket: "bucket"}) - if len(clients) != 1 { - t.Error("Expected one client created") - } - }) - - t.Run("Get S3 values from MountedSecret with KMS", func(t *testing.T) { - clients := factory.S3Clients(&config.S3{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}, AWSConfig: config.AWSConfig{Endpoint: "endpoint", Region: "region"}, Bucket: "bucket", BucketKeyEnabled: true, ServerSideEncryption: "aws:kms"}) - if len(clients) != 1 { - t.Error("Expected one client created") - } - }) - - t.Run("Get Kinesis values from MountedSecret", func(t *testing.T) { - clients := factory.KinesisClients(&config.Kinesis{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}, AWSConfig: config.AWSConfig{Endpoint: "endpoint", Region: "region"}, StreamName: "stream"}) - if len(clients) != 1 { - t.Error("Expected one client created") - } - }) - - t.Run("Get GCS values from MountedSecret", func(t *testing.T) { - clients := factory.GCSClients(&config.GCS{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}, Bucket: "bucket"}) - if len(clients) != 1 { - t.Error("Expected one client created") - } - }) - - t.Run("Get none existing mounted secret skips target", func(t *testing.T) { - clients := factory.LokiClients(&config.Loki{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: "no-exists"}}) - if len(clients) != 0 { - t.Error("Expected client are skipped") - } - }) -} diff --git a/pkg/crd/api/policyreport/v1alpha2/clusterpolicyreport_types.go b/pkg/crd/api/policyreport/v1alpha2/clusterpolicyreport_types.go index 2c4a47e1..e1f12abb 100755 --- a/pkg/crd/api/policyreport/v1alpha2/clusterpolicyreport_types.go +++ b/pkg/crd/api/policyreport/v1alpha2/clusterpolicyreport_types.go @@ -68,6 +68,16 @@ func (r *ClusterPolicyReport) GetResults() []PolicyReportResult { return r.Results } +func (r *ClusterPolicyReport) HasResult(id string) bool { + for _, r := range r.Results { + if r.GetID() == id { + return true + } + } + + return false +} + func (r *ClusterPolicyReport) SetResults(results []PolicyReportResult) { r.Results = results } @@ -91,6 +101,10 @@ func (r *ClusterPolicyReport) GetID() string { return strconv.FormatUint(h1, 10) } +func (r *ClusterPolicyReport) GetKey() string { + return r.Name +} + func (r *ClusterPolicyReport) GetKinds() []string { if r.GetScope() != nil { return []string{r.Scope.Kind} diff --git a/pkg/crd/api/policyreport/v1alpha2/clusterpolicyreport_types_test.go b/pkg/crd/api/policyreport/v1alpha2/clusterpolicyreport_types_test.go index 6fb0d76d..07e748dc 100644 --- a/pkg/crd/api/policyreport/v1alpha2/clusterpolicyreport_types_test.go +++ b/pkg/crd/api/policyreport/v1alpha2/clusterpolicyreport_types_test.go @@ -3,9 +3,10 @@ package v1alpha2_test import ( "testing" - "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" ) func TestClusterPolicyReport(t *testing.T) { diff --git a/pkg/crd/api/policyreport/v1alpha2/common.go b/pkg/crd/api/policyreport/v1alpha2/common.go index 1cb560e3..f5a4389c 100644 --- a/pkg/crd/api/policyreport/v1alpha2/common.go +++ b/pkg/crd/api/policyreport/v1alpha2/common.go @@ -14,7 +14,6 @@ limitations under the License. package v1alpha2 import ( - "bytes" "fmt" "strings" @@ -42,90 +41,6 @@ const ( SeverityInfo = "info" ) -// Priority Enum for internal Result weighting -type Priority int - -const ( - DefaultPriority Priority = iota - DebugPriority - InfoPriority - WarningPriority - CriticalPriority - ErrorPriority -) - -const ( - defaultString = "" - debugString = "debug" - infoString = "info" - warningString = "warning" - errorString = "error" - criticalString = "critical" -) - -// String maps the internal weighting of Priorities to a String representation -func (p Priority) String() string { - switch p { - case DebugPriority: - return debugString - case InfoPriority: - return infoString - case WarningPriority: - return warningString - case ErrorPriority: - return errorString - case CriticalPriority: - return criticalString - default: - return defaultString - } -} - -// MarshalJSON marshals the enum as a quoted json string -func (p Priority) MarshalJSON() ([]byte, error) { - buffer := bytes.NewBufferString(`"`) - buffer.WriteString(p.String()) - buffer.WriteString(`"`) - - return buffer.Bytes(), nil -} - -// NewPriority creates a new Priority based an its string representation -func NewPriority(p string) Priority { - switch p { - case debugString: - return DebugPriority - case infoString: - return InfoPriority - case warningString: - return WarningPriority - case errorString: - return ErrorPriority - case criticalString: - return CriticalPriority - default: - return DefaultPriority - } -} - -// PriorityFromSeverity creates a Priority based on a Severity -func PriorityFromSeverity(s PolicySeverity) Priority { - switch s { - case SeverityCritical: - return CriticalPriority - case SeverityHigh: - return ErrorPriority - case SeverityMedium: - return WarningPriority - case SeverityInfo: - return InfoPriority - case SeverityLow: - return InfoPriority - default: - return DebugPriority - } -} - // PolicyReportSummary provides a status count summary type PolicyReportSummary struct { // Pass provides the count of policies whose requirements were met @@ -169,6 +84,15 @@ type PolicyResult string // - info type PolicySeverity string +var SeverityLevel = map[PolicySeverity]int{ + "": -1, + SeverityInfo: 0, + SeverityLow: 1, + SeverityMedium: 2, + SeverityHigh: 3, + SeverityCritical: 4, +} + // PolicyReportResult provides the result for an individual policy type PolicyReportResult struct { ID string `json:"-"` @@ -216,8 +140,6 @@ type PolicyReportResult struct { // Severity indicates policy check result criticality // +optional Severity PolicySeverity `json:"severity,omitempty"` - - Priority Priority `json:"-"` } func (r *PolicyReportResult) GetResource() *corev1.ObjectReference { @@ -277,8 +199,10 @@ func ToResourceString(res *corev1.ObjectReference) string { type ReportInterface interface { metav1.Object GetID() string + GetKey() string GetScope() *corev1.ObjectReference GetResults() []PolicyReportResult + HasResult(id string) bool GetSummary() PolicyReportSummary GetSource() string GetKinds() []string diff --git a/pkg/crd/api/policyreport/v1alpha2/common_test.go b/pkg/crd/api/policyreport/v1alpha2/common_test.go deleted file mode 100644 index 37a0be2f..00000000 --- a/pkg/crd/api/policyreport/v1alpha2/common_test.go +++ /dev/null @@ -1,175 +0,0 @@ -package v1alpha2_test - -import ( - "encoding/json" - "testing" - - "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" - "github.com/kyverno/policy-reporter/pkg/report/result" - corev1 "k8s.io/api/core/v1" -) - -func TestCommon(t *testing.T) { - t.Run("Priority.String", func(t *testing.T) { - if v1alpha2.DefaultPriority.String() != "" { - t.Error("unexpected default priority mapping") - } - - if v1alpha2.DebugPriority.String() != "debug" { - t.Error("unexpected debug priority mapping") - } - - if v1alpha2.InfoPriority.String() != "info" { - t.Error("unexpected info mapping") - } - - if v1alpha2.WarningPriority.String() != "warning" { - t.Error("unexpected warning mapping") - } - - if v1alpha2.ErrorPriority.String() != "error" { - t.Error("unexpected error mapping") - } - - if v1alpha2.CriticalPriority.String() != "critical" { - t.Error("unexpected critical mapping") - } - }) - - t.Run("Priority.MarshalJSON", func(t *testing.T) { - v, err := json.Marshal(v1alpha2.WarningPriority) - if err != nil { - t.Fatalf("unexpected marshal error: %s", err.Error()) - } - - if string(v) != `"warning"` { - t.Fatalf("unexpected marshal value: %s", v) - } - }) - - t.Run("NewPriority", func(t *testing.T) { - if v1alpha2.NewPriority("") != v1alpha2.DefaultPriority { - t.Error("unexpected prioriry created") - } - - if v1alpha2.NewPriority("debug") != v1alpha2.DebugPriority { - t.Error("unexpected prioriry created") - } - - if v1alpha2.NewPriority("info") != v1alpha2.InfoPriority { - t.Error("unexpected prioriry created") - } - - if v1alpha2.NewPriority("warning") != v1alpha2.WarningPriority { - t.Error("unexpected prioriry created") - } - - if v1alpha2.NewPriority("error") != v1alpha2.ErrorPriority { - t.Error("unexpected prioriry created") - } - - if v1alpha2.NewPriority("critical") != v1alpha2.CriticalPriority { - t.Error("unexpected prioriry created") - } - }) - - t.Run("PriorityFromSeverity", func(t *testing.T) { - if v1alpha2.PriorityFromSeverity(v1alpha2.SeverityCritical) != v1alpha2.CriticalPriority { - t.Error("unexpected prioriry created") - } - - if v1alpha2.PriorityFromSeverity(v1alpha2.SeverityHigh) != v1alpha2.ErrorPriority { - t.Error("unexpected prioriry created") - } - - if v1alpha2.PriorityFromSeverity(v1alpha2.SeverityMedium) != v1alpha2.WarningPriority { - t.Error("unexpected prioriry created") - } - - if v1alpha2.PriorityFromSeverity(v1alpha2.SeverityInfo) != v1alpha2.InfoPriority { - t.Error("unexpected prioriry created") - } - - if v1alpha2.PriorityFromSeverity(v1alpha2.SeverityLow) != v1alpha2.InfoPriority { - t.Error("unexpected prioriry created") - } - if v1alpha2.PriorityFromSeverity("") != v1alpha2.DebugPriority { - t.Error("unexpected prioriry created") - } - }) -} - -func TestPolicyReportResult(t *testing.T) { - t.Run("GetResource Without Resources", func(t *testing.T) { - r := &v1alpha2.PolicyReportResult{} - - if r.GetResource() != nil { - t.Error("expected nil resource for empty result") - } - }) - t.Run("GetResource With Resources", func(t *testing.T) { - r := &v1alpha2.PolicyReportResult{Resources: []corev1.ObjectReference{{Name: "test"}}} - - if r.GetResource().Name != "test" { - t.Error("expected result resource returned") - } - }) - t.Run("GetKind Without Resource", func(t *testing.T) { - r := &v1alpha2.PolicyReportResult{} - - if r.GetKind() != "" { - t.Error("expected result kind to be empty string") - } - }) - t.Run("GetKind", func(t *testing.T) { - r := &v1alpha2.PolicyReportResult{Resources: []corev1.ObjectReference{{Name: "test", Kind: "Pod"}}} - - if r.GetKind() != "Pod" { - t.Error("expected result kind to be Pod") - } - }) - t.Run("GetID from Result With Resource", func(t *testing.T) { - r := v1alpha2.PolicyReportResult{Resources: []corev1.ObjectReference{{Name: "test", Kind: "Pod"}}} - r.ID = result.NewIDGenerator(nil).Generate(&v1alpha2.PolicyReport{}, r) - - if r.GetID() != "18007334074686647077" { - t.Errorf("expected result kind to be '18007334074686647077', got :%s", r.GetID()) - } - }) - t.Run("GetID from Result With ID Property", func(t *testing.T) { - r := v1alpha2.PolicyReportResult{Resources: []corev1.ObjectReference{{Name: "test", Kind: "Pod"}}, Properties: map[string]string{"resultID": "result-id"}} - r.ID = result.NewIDGenerator(nil).Generate(&v1alpha2.PolicyReport{}, r) - - if r.GetID() != "result-id" { - t.Errorf("expected result kind to be 'result-id', got :%s", r.GetID()) - } - }) - t.Run("ToResourceString with Namespace and Kind", func(t *testing.T) { - r := &v1alpha2.PolicyReportResult{Resources: []corev1.ObjectReference{{Name: "test", Namespace: "default", Kind: "Pod"}}} - - if r.ResourceString() != "default/pod/test" { - t.Errorf("expected result resource string 'default/pod/name', got: %s", r.ResourceString()) - } - }) - t.Run("ToResourceString with Kind", func(t *testing.T) { - r := &v1alpha2.PolicyReportResult{Resources: []corev1.ObjectReference{{Name: "test", Kind: "Namespace"}}} - - if r.ResourceString() != "namespace/test" { - t.Errorf("expected result resource string 'namespace/test', got: %s", r.ResourceString()) - } - }) - t.Run("ToResourceString with Name", func(t *testing.T) { - r := &v1alpha2.PolicyReportResult{Resources: []corev1.ObjectReference{{Name: "test"}}} - - if r.ResourceString() != "test" { - t.Errorf("expected result resource string 'test', got :%s", r.ResourceString()) - } - }) - t.Run("ToResourceString Without Resource", func(t *testing.T) { - r := &v1alpha2.PolicyReportResult{} - - if r.ResourceString() != "" { - t.Errorf("expected result resource string to be empty, got :%s", r.ResourceString()) - } - }) -} diff --git a/pkg/crd/api/policyreport/v1alpha2/policyreport_types.go b/pkg/crd/api/policyreport/v1alpha2/policyreport_types.go index 3bae90d5..eacadd38 100644 --- a/pkg/crd/api/policyreport/v1alpha2/policyreport_types.go +++ b/pkg/crd/api/policyreport/v1alpha2/policyreport_types.go @@ -14,6 +14,7 @@ limitations under the License. package v1alpha2 import ( + "fmt" "strconv" "github.com/segmentio/fasthash/fnv1a" @@ -64,6 +65,16 @@ func (r *PolicyReport) GetResults() []PolicyReportResult { return r.Results } +func (r *PolicyReport) HasResult(id string) bool { + for _, r := range r.Results { + if r.GetID() == id { + return true + } + } + + return false +} + func (r *PolicyReport) SetResults(results []PolicyReportResult) { r.Results = results } @@ -125,6 +136,10 @@ func (r *PolicyReport) GetID() string { return strconv.FormatUint(h1, 10) } +func (r *PolicyReport) GetKey() string { + return fmt.Sprintf("%s/%s", r.Namespace, r.Name) +} + func (r *PolicyReport) GetScope() *corev1.ObjectReference { return r.Scope } diff --git a/pkg/crd/api/policyreport/v1alpha2/policyreport_types_test.go b/pkg/crd/api/policyreport/v1alpha2/policyreport_types_test.go index ea3aa735..8a362924 100644 --- a/pkg/crd/api/policyreport/v1alpha2/policyreport_types_test.go +++ b/pkg/crd/api/policyreport/v1alpha2/policyreport_types_test.go @@ -3,9 +3,10 @@ package v1alpha2_test import ( "testing" - "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" ) func TestPolicyReport(t *testing.T) { diff --git a/pkg/database/builder.go b/pkg/database/builder.go new file mode 100644 index 00000000..06ffe482 --- /dev/null +++ b/pkg/database/builder.go @@ -0,0 +1,229 @@ +package database + +import ( + "context" + "fmt" + "strings" + + "github.com/uptrace/bun" + "github.com/uptrace/bun/dialect" +) + +type QueryBuilder struct { + query *bun.SelectQuery +} + +func (q *QueryBuilder) Filter(column string, values []string) *QueryBuilder { + if len(values) > 1 { + q.query.Where(column+" IN (?)", bun.In(values)) + } else if len(values) == 1 { + q.query.Where(column+" = ?", values[0]) + } + + return q +} + +func (q *QueryBuilder) Scoped(scoped bool) *QueryBuilder { + if scoped { + return q.NamespaceScope() + } + + return q.ClusterScope() +} + +func (q *QueryBuilder) FilterValue(column string, value string) *QueryBuilder { + if value != "" { + q.query.Where(column+" = ?", value) + } + + return q +} + +func (q *QueryBuilder) WithEmpty(column string) *QueryBuilder { + q.query.Where(column + " = ''") + + return q +} + +func (q *QueryBuilder) WithNotEmpty(column string) *QueryBuilder { + q.query.Where(column + " != ''") + + return q +} + +func (q *QueryBuilder) Exclude(filter Filter, prefix string) *QueryBuilder { + if filter.ResourceID == "" && len(filter.Kinds) == 0 && len(filter.Exclude) > 0 { + for source, kind := range filter.Exclude { + q.query.Where(fmt.Sprintf("(%s.source != ? OR (%s.source = ? AND %s.resource_kind NOT IN (?)))", prefix, prefix, prefix), source, source, bun.In(kind)) + } + } + + return q +} + +func (q *QueryBuilder) ResourceSearch(value string) *QueryBuilder { + if value != "" { + q.query.Where(`(resource_name LIKE ?0 OR LOWER(resource_kind) = LOWER(?1))`, "%"+value+"%", value) + } + + return q +} + +func (q *QueryBuilder) PolicySearch(value string) *QueryBuilder { + if value != "" { + q.query.Where(`(f.policy LIKE ?0 OR f.severity LIKE ?0 OR LOWER(f.resource_kind) = LOWER(?1))`, "%"+value+"%", value) + } + + return q +} + +func (q *QueryBuilder) ResultSearch(value string) *QueryBuilder { + if value != "" { + q.query.Where(`(resource_namespace LIKE ?0 OR resource_name LIKE ?0 OR policy LIKE ?0 OR rule LIKE ?0 OR severity = ?1 OR result = ?1 OR LOWER(resource_kind) = LOWER(?1))`, "%"+value+"%", value) + } + + return q +} + +func (q *QueryBuilder) FilterMap(columns map[string][]string) *QueryBuilder { + for column, values := range columns { + if len(values) > 1 { + q.query.Where(column+" IN (?)", bun.In(values)) + } else if len(values) == 1 { + q.query.Where(column+" = ?", values[0]) + } + } + + return q +} + +func (q *QueryBuilder) FilterOptionalNamespaces(values []string) *QueryBuilder { + if len(values) > 1 { + q.query.Where("(resource_namespace IN (?) OR resource_namespace = '')", bun.In(values)) + } else if len(values) == 1 { + q.query.Where("(resource_namespace = ? OR resource_namespace = '')", values[0]) + } + + return q +} + +func (q *QueryBuilder) NamespaceScope() *QueryBuilder { + q.query.Where("resource_namespace != ''") + + return q +} + +func (q *QueryBuilder) ClusterScope() *QueryBuilder { + q.query.Where("resource_namespace = ''") + + return q +} + +func (q *QueryBuilder) Scan(ctx context.Context, dest ...any) error { + return q.query.Scan(ctx, dest...) +} + +func (q *QueryBuilder) Columns(columns ...string) *QueryBuilder { + q.query.Column(columns...) + + return q +} + +func (q *QueryBuilder) Group(columns ...string) *QueryBuilder { + q.query.Group(columns...) + + return q +} + +func (q *QueryBuilder) Order(orders ...string) *QueryBuilder { + q.query.Order(orders...) + + return q +} + +func (q *QueryBuilder) SelectStatusSummaries() *QueryBuilder { + q.query.ColumnExpr("SUM(res.pass) as pass, SUM(res.warn) as warn, SUM(res.fail) as fail, SUM(res.error) as error, SUM(res.skip) as skip") + + return q +} + +func (q *QueryBuilder) SelectSeveritySummaries() *QueryBuilder { + q.query.ColumnExpr("SUM(res.info) as info, SUM(res.low) as low, SUM(res.medium) as medium, SUM(res.high) as high, SUM(res.critical) as critical, SUM(res.unknown) as unknown") + + return q +} + +func (q *QueryBuilder) Pagination(pagination Pagination) *QueryBuilder { + q.query.OrderExpr(fmt.Sprintf( + "%s %s", + strings.Join(pagination.SortBy, ","), + pagination.Direction, + )) + + if pagination.Page == 0 || pagination.Offset == 0 { + return q + } + + q.query.Limit(pagination.Offset) + q.query.Offset((pagination.Page - 1) * pagination.Offset) + + return q +} + +func (q *QueryBuilder) FilterReportLabels(labels map[string]string) *QueryBuilder { + if len(labels) > 0 { + q.query.Join("JOIN policy_report AS pr ON pr.id = policy_report_id") + + for key, value := range labels { + q.query.Where(fmt.Sprintf(q.jsonExtractLayout(), key), value) + } + } + + return q +} + +func (q *QueryBuilder) FilterLabels(labels map[string]string) *QueryBuilder { + if len(labels) > 0 { + q.query.Join("JOIN policy_report AS pr ON pr.id = policy_report_id") + + for key, value := range labels { + q.query.Where(fmt.Sprintf(q.jsonExtractLayout(), key), value) + } + } + + return q +} + +func (q *QueryBuilder) jsonExtractLayout() string { + if q.query.Dialect().Name() == dialect.PG { + return "(pr.labels->>'%s') = ?" + } + + return "json_extract(pr.labels, '$.\"%s\"') = ?" +} + +func (q *QueryBuilder) GetQuery() *bun.SelectQuery { + return q.query +} + +func FromQuery(query *bun.SelectQuery) *QueryBuilder { + return &QueryBuilder{query: query} +} + +func NewFilterQuery(db *bun.DB, column string) *QueryBuilder { + return &QueryBuilder{ + query: db. + NewSelect(). + TableExpr("policy_report_filter as f"). + Column(column). + Distinct(). + Order(column + " ASC"). + Where(column + " != ''"), + } +} + +func NewResourceQuery(db *bun.DB) *QueryBuilder { + return &QueryBuilder{ + query: db.NewSelect().TableExpr("policy_report_resource as res").Distinct(), + } +} diff --git a/pkg/database/bun.go b/pkg/database/bun.go index 3c13c63b..0bc03dc5 100644 --- a/pkg/database/bun.go +++ b/pkg/database/bun.go @@ -3,11 +3,9 @@ package database import ( "context" "database/sql" - "encoding/json" "errors" "fmt" "os" - "strings" "time" _ "github.com/mattn/go-sqlite3" @@ -19,7 +17,6 @@ import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - api "github.com/kyverno/policy-reporter/pkg/api/v1" "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" "github.com/kyverno/policy-reporter/pkg/report" ) @@ -36,10 +33,831 @@ const ( type Store struct { db *bun.DB version string - - jsonExtractLayout string } +/////////////////////////////// +/// V1 API Queries /// +/////////////////////////////// + +func (s *Store) FetchPolicyReports(ctx context.Context, filter Filter, pagination Pagination) ([]PolicyReport, error) { + list := make([]PolicyReport, 0) + + err := FromQuery(s.db.NewSelect().Model(&list)). + FilterMap(map[string][]string{ + "pr.source": filter.Sources, + "pr.namespace": filter.Namespaces, + }). + FilterLabels(filter.ReportLabel). + FilterValue("pr.type", report.PolicyReportType). + Pagination(pagination). + Scan(ctx) + + return list, err +} + +func (s *Store) CountPolicyReports(ctx context.Context, filter Filter) (int, error) { + return FromQuery(s.db.NewSelect().Model((*PolicyReport)(nil))). + FilterMap(map[string][]string{ + "pr.source": filter.Sources, + "pr.namespace": filter.Namespaces, + }). + FilterLabels(filter.ReportLabel). + FilterValue("pr.type", report.PolicyReportType). + GetQuery(). + Count(ctx) +} + +func (s *Store) FetchClusterPolicyReports(ctx context.Context, filter Filter, pagination Pagination) ([]PolicyReport, error) { + list := make([]PolicyReport, 0) + + err := FromQuery(s.db.NewSelect().Model(&list)). + FilterMap(map[string][]string{ + "pr.source": filter.Sources, + }). + FilterLabels(filter.ReportLabel). + FilterValue("pr.type", report.ClusterPolicyReportType). + Pagination(pagination). + Scan(ctx) + + return list, err +} + +func (s *Store) CountClusterPolicyReports(ctx context.Context, filter Filter) (int, error) { + return FromQuery(s.db.NewSelect().Model((*PolicyReport)(nil))). + FilterMap(map[string][]string{ + "pr.source": filter.Sources, + }). + FilterLabels(filter.ReportLabel). + FilterValue("pr.type", report.ClusterPolicyReportType). + GetQuery(). + Count(ctx) +} + +func (s *Store) FetchRuleStatusCounts(ctx context.Context, policy, rule string) ([]StatusCount, error) { + counts := make([]StatusCount, 0) + + err := s.db.NewSelect(). + Table("policy_report_result"). + ColumnExpr("COUNT(id) as count, result as status"). + Where("rule = ?", rule). + Where("policy = ?", policy). + Group("status"). + Scan(ctx, &counts) + + return counts, err +} + +func (s *Store) FetchNamespaces(ctx context.Context, filter Filter) ([]string, error) { + return s.FetchNamespacedFilter(ctx, "resource_namespace", filter) +} + +func (s *Store) FetchNamespacedFilter(ctx context.Context, column string, filter Filter) ([]string, error) { + list := make([]string, 0) + + err := NewFilterQuery(s.db, "f."+column). + FilterMap(map[string][]string{ + "f.result": filter.Status, + "f.source": filter.Sources, + "f.category": filter.Categories, + "f.policy": filter.Policies, + "f.resource_kind": filter.Kinds, + "f.resource_namespace": filter.Namespaces, + }). + FilterReportLabels(filter.ReportLabel). + NamespaceScope(). + Scan(ctx, &list) + + return list, err +} + +func (s *Store) FetchClusterFilter(ctx context.Context, column string, filter Filter) ([]string, error) { + list := make([]string, 0) + + err := NewFilterQuery(s.db, "f."+column). + FilterMap(map[string][]string{ + "f.source": filter.Sources, + "f.category": filter.Categories, + "f.policy": filter.Policies, + "f.resource_kind": filter.Kinds, + }). + FilterReportLabels(filter.ReportLabel). + ClusterScope(). + Scan(ctx, &list) + + return list, err +} + +func (s *Store) FetchNamespacedResources(ctx context.Context, filter Filter) ([]ResourceResult, error) { + list := make([]ResourceResult, 0) + + err := FromQuery(s.db.NewSelect().Model(&list).Distinct()). + Columns("resource_name", "resource_kind", "resource_namespace"). + FilterMap(map[string][]string{ + "res.source": filter.Sources, + "res.category": filter.Categories, + "res.policy": filter.Policies, + "res.resource_kind": filter.Kinds, + "res.resource_namespace": filter.Namespaces, + }). + FilterReportLabels(filter.ReportLabel). + NamespaceScope(). + Scan(ctx, &list) + + return list, err +} + +func (s *Store) FetchClusterResources(ctx context.Context, filter Filter) ([]ResourceResult, error) { + list := make([]ResourceResult, 0) + + err := FromQuery(s.db.NewSelect().Model(&list).Distinct()). + Columns("resource_name", "resource_kind"). + FilterMap(map[string][]string{ + "f.source": filter.Sources, + "f.category": filter.Categories, + "f.policy": filter.Policies, + "f.resource_kind": filter.Kinds, + "f.resource_namespace": filter.Namespaces, + }). + FilterReportLabels(filter.ReportLabel). + ClusterScope(). + Scan(ctx, &list) + + return list, err +} + +func (s *Store) FetchClusterScopedStatusCounts(ctx context.Context, filter Filter) ([]StatusCount, error) { + list := make([]StatusCount, 0) + + err := FromQuery(s.db.NewSelect().Model(&list).ColumnExpr("SUM(f.count) as count, f.result as status")). + FilterMap(map[string][]string{ + "f.source": filter.Sources, + "f.category": filter.Categories, + "f.policy": filter.Policies, + "f.resource_kind": filter.Kinds, + "f.result": filter.Status, + "f.severity": filter.Severities, + }). + FilterReportLabels(filter.ReportLabel). + ClusterScope(). + Group("status"). + Scan(ctx) + + return list, err +} + +func (s *Store) FetchNamespaceScopedStatusCounts(ctx context.Context, filter Filter) ([]StatusCount, error) { + list := make([]StatusCount, 0) + + err := FromQuery(s.db.NewSelect().Model(&list).ColumnExpr("SUM(f.count) as count, f.result as status, f.resource_namespace")). + FilterMap(map[string][]string{ + "f.source": filter.Sources, + "f.category": filter.Categories, + "f.policy": filter.Policies, + "f.resource_kind": filter.Kinds, + "f.resource_namespace": filter.Namespaces, + "f.result": filter.Status, + "f.severity": filter.Severities, + }). + FilterReportLabels(filter.ReportLabel). + NamespaceScope(). + Group("status", "f.resource_namespace"). + Scan(ctx) + + return list, err +} + +/////////////////////////////// +/// V2 API Queries /// +/////////////////////////////// + +func (s *Store) FetchSources(ctx context.Context, filter Filter) ([]string, error) { + list := make([]string, 0) + + err := NewFilterQuery(s.db, "f.source"). + FilterMap(map[string][]string{ + "f.resource_kind": filter.Kinds, + }). + FilterValue("id", filter.ResourceID). + FilterReportLabels(filter.ReportLabel). + Scan(ctx, &list) + + return list, err +} + +func (s *Store) FetchCategories(ctx context.Context, filter Filter) ([]Category, error) { + list := make([]Category, 0) + + err := FromQuery(s.db.NewSelect().Model(&list).ColumnExpr("SUM(f.count) as count")). + Columns("f.source", "f.category", "f.result", "f.severity"). + FilterMap(map[string][]string{ + "f.source": filter.Sources, + "f.category": filter.Categories, + "f.resource_kind": filter.Kinds, + "f.resource_namespace": filter.Namespaces, + }). + Exclude(filter, "f"). + FilterReportLabels(filter.ReportLabel). + Group("f.source", "f.category", "f.result", "f.severity"). + Order("f.source ASC", "f.category ASC"). + Scan(ctx, &list) + + return list, err +} + +func (s *Store) FetchResource(ctx context.Context, id string) (ResourceResult, error) { + result := ResourceResult{} + + err := FromQuery(s.db.NewSelect().Model(&result)). + Columns("res.id", "resource_uid", "resource_kind", "resource_api_version", "resource_namespace", "resource_name", "res.source", "res.category"). + SelectStatusSummaries(). + FilterValue("res.id", id). + GetQuery(). + Limit(1). + Scan(ctx) + + return result, err +} + +func (s *Store) FetchResourceCategories(ctx context.Context, resource string, filter Filter) ([]ResourceCategory, error) { + list := make([]ResourceCategory, 0) + + err := FromQuery(s.db.NewSelect().Model(&list)). + Columns("res.source", "res.category"). + SelectStatusSummaries(). + FilterMap(map[string][]string{ + "res.source": filter.Sources, + "res.category": filter.Categories, + }). + Exclude(filter, "res"). + FilterValue("id", resource). + FilterReportLabels(filter.ReportLabel). + Group("res.source", "res.category"). + Order("res.source ASC", "res.category ASC"). + Scan(ctx, &list) + + return list, err +} + +func (s *Store) FetchProperty(ctx context.Context, property string, filter Filter) ([]ResultProperty, error) { + result := make([]ResultProperty, 0) + + err := FromQuery( + s.db.NewSelect(). + Model(&result). + Distinct(). + ColumnExpr(fmt.Sprintf("resource_namespace, properties->>'%s' as property", property))). + FilterMap(map[string][]string{ + "category": filter.Categories, + "source": filter.Sources, + "policy": filter.Policies, + "rules": filter.Rules, + "status": filter.Status, + "resource_namespace": filter.Namespaces, + }). + GetQuery(). + Where(fmt.Sprintf("properties->'%s' IS NOT NULL", property)). + Scan(ctx) + + return result, err +} + +func (s *Store) FetchResourceStatusCounts(ctx context.Context, id string, filter Filter) ([]ResourceStatusCount, error) { + result := []ResourceStatusCount{} + + err := FromQuery(s.db.NewSelect().Model(&result)). + Columns("res.source"). + SelectStatusSummaries(). + FilterMap(map[string][]string{ + "res.category": filter.Categories, + "res.source": filter.Sources, + "policy": filter.Policies, + }). + FilterValue("res.id", id). + FilterReportLabels(filter.ReportLabel). + Group("res.source"). + Scan(ctx) + + return result, err +} + +func (s *Store) FetchResourceSeverityCounts(ctx context.Context, id string, filter Filter) ([]ResourceSeverityCount, error) { + result := []ResourceSeverityCount{} + + err := FromQuery(s.db.NewSelect().Model(&result)). + Columns("res.source"). + SelectSeveritySummaries(). + FilterMap(map[string][]string{ + "res.category": filter.Categories, + "res.source": filter.Sources, + "policy": filter.Policies, + }). + FilterValue("res.id", id). + FilterReportLabels(filter.ReportLabel). + Group("res.source"). + Scan(ctx) + + return result, err +} + +func (s *Store) FetchNamespaceResourceResults(ctx context.Context, filter Filter, pagination Pagination) ([]ResourceResult, error) { + results := make([]ResourceResult, 0) + + err := FromQuery(s.db.NewSelect().Model(&results)). + Columns("res.id", "resource_uid", "resource_kind", "resource_api_version", "resource_namespace", "resource_name"). + SelectStatusSummaries(). + SelectSeveritySummaries(). + Group("res.id", "resource_uid", "resource_kind", "resource_api_version", "resource_namespace", "resource_name"). + FilterMap(map[string][]string{ + "source": filter.Sources, + "category": filter.Categories, + "resource_namespace": filter.Namespaces, + "resource_kind": filter.Kinds, + }). + FilterValue("res.id", filter.ResourceID). + ResourceSearch(filter.Search). + FilterReportLabels(filter.ReportLabel). + Exclude(filter, "res"). + NamespaceScope(). + Pagination(pagination). + Scan(ctx) + + return results, err +} + +func (s *Store) CountNamespaceResourceResults(ctx context.Context, filter Filter) (int, error) { + query := FromQuery(s.db.NewSelect().Model((*ResourceResult)(nil))). + FilterMap(map[string][]string{ + "source": filter.Sources, + "category": filter.Categories, + "resource_namespace": filter.Namespaces, + "resource_kind": filter.Kinds, + }). + FilterValue("res.id", filter.ResourceID). + ResourceSearch(filter.Search). + FilterReportLabels(filter.ReportLabel). + Exclude(filter, "res"). + NamespaceScope(). + Group("res.id"). + GetQuery() + + return query.Count(ctx) +} + +func (s *Store) FetchClusterResourceResults(ctx context.Context, filter Filter, pagination Pagination) ([]ResourceResult, error) { + results := make([]ResourceResult, 0) + + err := FromQuery(s.db.NewSelect().Model(&results)). + Columns("res.id", "resource_uid", "resource_kind", "resource_api_version", "resource_namespace", "resource_name"). + SelectStatusSummaries(). + SelectSeveritySummaries(). + Group("res.id", "resource_uid", "resource_kind", "resource_api_version", "resource_namespace", "resource_name"). + FilterMap(map[string][]string{ + "source": filter.Sources, + "category": filter.Categories, + "resource_kind": filter.Kinds, + }). + FilterValue("res.id", filter.ResourceID). + ResourceSearch(filter.Search). + FilterReportLabels(filter.ReportLabel). + Exclude(filter, "res"). + ClusterScope(). + Pagination(pagination). + Scan(ctx) + + return results, err +} + +func (s *Store) CountClusterResourceResults(ctx context.Context, filter Filter) (int, error) { + query := FromQuery(s.db.NewSelect().Model((*ResourceResult)(nil))). + FilterMap(map[string][]string{ + "source": filter.Sources, + "category": filter.Categories, + "resource_kind": filter.Kinds, + }). + FilterValue("res.id", filter.ResourceID). + ResourceSearch(filter.Search). + FilterReportLabels(filter.ReportLabel). + Exclude(filter, "res"). + ClusterScope(). + Group("res.id"). + GetQuery() + + return query.Count(ctx) +} + +func (s *Store) FetchResourceResults(ctx context.Context, id string, filter Filter) ([]ResourceResult, error) { + results := make([]ResourceResult, 0) + + err := FromQuery(s.db.NewSelect().Model(&results)). + Columns("res.id", "resource_uid", "resource_kind", "resource_api_version", "resource_namespace", "resource_name", "res.source"). + SelectStatusSummaries(). + FilterValue(`res.id`, id). + FilterMap(map[string][]string{ + "source": filter.Sources, + "category": filter.Categories, + "resource_kind": filter.Kinds, + }). + FilterReportLabels(filter.ReportLabel). + ResourceSearch(filter.Search). + Order("res.source ASC"). + Group("res.id", "resource_uid", "resource_kind", "resource_api_version", "resource_namespace", "resource_name", "res.source"). + Scan(ctx) + + return results, err +} + +func (s *Store) FetchResourcePolicyResults(ctx context.Context, id string, filter Filter, pagination Pagination) ([]PolicyReportResult, error) { + results := make([]PolicyReportResult, 0) + + err := FromQuery(s.db.NewSelect().Model(&results)). + FilterValue(`r.resource_id`, id). + FilterMap(map[string][]string{ + "source": filter.Sources, + "category": filter.Categories, + }). + ResultSearch(filter.Search). + FilterReportLabels(filter.ReportLabel). + Pagination(pagination). + Scan(ctx) + + return results, err +} + +func (s *Store) CountResourcePolicyResults(ctx context.Context, id string, filter Filter) (int, error) { + return FromQuery(s.db.NewSelect().Model((*PolicyReportResult)(nil))). + FilterValue(`r.resource_id`, id). + FilterMap(map[string][]string{ + "source": filter.Sources, + "category": filter.Categories, + }). + ResultSearch(filter.Search). + FilterReportLabels(filter.ReportLabel). + GetQuery(). + Count(ctx) +} + +func (s *Store) FetchResults(ctx context.Context, namespaced bool, filter Filter, pagination Pagination) ([]PolicyReportResult, error) { + results := make([]PolicyReportResult, 0) + + err := FromQuery(s.db.NewSelect().Model(&results)). + FilterMap(map[string][]string{ + "source": filter.Sources, + "category": filter.Categories, + "policy": filter.Policies, + "rule": filter.Rules, + "resource_namespace": filter.Namespaces, + "resource_kind": filter.Kinds, + "resource_name": filter.Resources, + "result": filter.Status, + "severity": filter.Severities, + }). + Scoped(namespaced). + FilterValue(`r.resource_id`, filter.ResourceID). + ResultSearch(filter.Search). + FilterReportLabels(filter.ReportLabel). + Exclude(filter, "r"). + Pagination(pagination). + Scan(ctx) + + return results, err +} + +func (s *Store) CountResults(ctx context.Context, namespaced bool, filter Filter) (int, error) { + return FromQuery(s.db.NewSelect().Model((*PolicyReportResult)(nil))). + FilterMap(map[string][]string{ + "source": filter.Sources, + "category": filter.Categories, + "policy": filter.Policies, + "rule": filter.Rules, + "resource_namespace": filter.Namespaces, + "resource_kind": filter.Kinds, + "resource_name": filter.Resources, + "result": filter.Status, + "severity": filter.Severities, + }). + Scoped(namespaced). + FilterValue(`r.resource_id`, filter.ResourceID). + ResultSearch(filter.Search). + FilterReportLabels(filter.ReportLabel). + Exclude(filter, "r"). + GetQuery(). + Count(ctx) +} + +func (s *Store) FetchResultsWithoutResource(ctx context.Context, filter Filter, pagination Pagination) ([]PolicyReportResult, error) { + results := make([]PolicyReportResult, 0) + + err := FromQuery(s.db.NewSelect().Model(&results)). + FilterMap(map[string][]string{ + "source": filter.Sources, + "category": filter.Categories, + "policy": filter.Policies, + "rule": filter.Rules, + "result": filter.Status, + "severity": filter.Severities, + }). + WithEmpty("resource_name"). + ResultSearch(filter.Search). + FilterReportLabels(filter.ReportLabel). + Exclude(filter, "r"). + Pagination(pagination). + Scan(ctx) + + return results, err +} + +func (s *Store) CountResultsWithoutResource(ctx context.Context, filter Filter) (int, error) { + return FromQuery(s.db.NewSelect().Model((*PolicyReportResult)(nil))). + FilterMap(map[string][]string{ + "source": filter.Sources, + "category": filter.Categories, + "policy": filter.Policies, + "rule": filter.Rules, + "result": filter.Status, + "severity": filter.Severities, + }). + WithEmpty("resource_name"). + ResultSearch(filter.Search). + FilterReportLabels(filter.ReportLabel). + Exclude(filter, "r"). + GetQuery(). + Count(ctx) +} + +func (s *Store) UseResources(ctx context.Context, source string, filter Filter) (bool, error) { + return FromQuery(s.db.NewSelect().Model((*PolicyReportResult)(nil))). + FilterValue("source", source). + FilterMap(map[string][]string{ + "category": filter.Categories, + "policy": filter.Policies, + "rule": filter.Rules, + }). + WithNotEmpty("resource_name"). + ResultSearch(filter.Search). + FilterReportLabels(filter.ReportLabel). + Exclude(filter, "r"). + GetQuery(). + Exists(ctx) +} + +func (s *Store) FetchClusterStatusCounts(ctx context.Context, source string, filter Filter) ([]StatusCount, error) { + results := make([]StatusCount, 0) + + err := FromQuery(s.db. + NewSelect(). + TableExpr("policy_report_filter as f"). + ColumnExpr("SUM(f.count) as count, f.result as status")). + FilterMap(map[string][]string{ + "category": filter.Categories, + "policy": filter.Policies, + "resource_kind": filter.Kinds, + }). + FilterValue("f.source", source). + FilterReportLabels(filter.ReportLabel). + Exclude(filter, "f"). + ClusterScope(). + Group("status"). + Scan(ctx, &results) + + return results, err +} + +func (s *Store) FetchClusterSeverityCounts(ctx context.Context, source string, filter Filter) ([]SeverityCount, error) { + results := make([]SeverityCount, 0) + + err := FromQuery(s.db. + NewSelect(). + TableExpr("policy_report_filter as f"). + ColumnExpr("SUM(f.count) as count, f.severity")). + FilterMap(map[string][]string{ + "category": filter.Categories, + "policy": filter.Policies, + "resource_kind": filter.Kinds, + }). + FilterValue("f.source", source). + FilterReportLabels(filter.ReportLabel). + Exclude(filter, "f"). + ClusterScope(). + Group("f.severity"). + Scan(ctx, &results) + + return results, err +} + +func (s *Store) FetchNamespaceStatusCounts(ctx context.Context, source string, filter Filter) ([]StatusCount, error) { + results := make([]StatusCount, 0) + + err := FromQuery(s.db. + NewSelect(). + TableExpr("policy_report_filter as f"). + ColumnExpr("f.resource_namespace, SUM(f.count) as count, f.result as status")). + FilterMap(map[string][]string{ + "f.category": filter.Categories, + "f.resource_kind": filter.Kinds, + "f.resource_namespace": filter.Namespaces, + "f.policy": filter.Policies, + "status": filter.Status, + }). + FilterValue("f.source", source). + FilterReportLabels(filter.ReportLabel). + Exclude(filter, "f"). + NamespaceScope(). + Group("f.resource_namespace", "status"). + Order("f.resource_namespace ASC", "status ASC"). + Scan(ctx, &results) + + return results, err +} + +func (s *Store) FetchNamespaceSeverityCounts(ctx context.Context, source string, filter Filter) ([]SeverityCount, error) { + results := make([]SeverityCount, 0) + + err := FromQuery(s.db. + NewSelect(). + TableExpr("policy_report_filter as f"). + ColumnExpr("f.resource_namespace, SUM(f.count) as count, f.severity")). + FilterMap(map[string][]string{ + "f.category": filter.Categories, + "f.resource_kind": filter.Kinds, + "f.resource_namespace": filter.Namespaces, + "f.policy": filter.Policies, + "f.severity": filter.Severities, + }). + FilterValue("f.source", source). + FilterReportLabels(filter.ReportLabel). + Exclude(filter, "f"). + NamespaceScope(). + Group("f.resource_namespace", "f.severity"). + Order("f.resource_namespace ASC", "f.severity ASC"). + Scan(ctx, &results) + + return results, err +} + +func (s *Store) FetchTotalStatusCounts(ctx context.Context, source string, filter Filter) ([]StatusCount, error) { + results := make([]StatusCount, 0) + + err := FromQuery(s.db. + NewSelect(). + TableExpr("policy_report_filter as f"). + ColumnExpr("SUM(f.count) as count, f.result as status")). + FilterMap(map[string][]string{ + "category": filter.Categories, + "policy": filter.Policies, + "resource_kind": filter.Kinds, + }). + FilterValue("f.source", source). + FilterReportLabels(filter.ReportLabel). + Exclude(filter, "f"). + Group("status"). + Scan(ctx, &results) + + return results, err +} + +func (s *Store) FetchTotalSeverityCounts(ctx context.Context, source string, filter Filter) ([]SeverityCount, error) { + results := make([]SeverityCount, 0) + + err := FromQuery(s.db. + NewSelect(). + TableExpr("policy_report_filter as f"). + ColumnExpr("SUM(f.count) as count, f.severity")). + FilterMap(map[string][]string{ + "category": filter.Categories, + "policy": filter.Policies, + "resource_kind": filter.Kinds, + }). + FilterValue("f.source", source). + FilterReportLabels(filter.ReportLabel). + Exclude(filter, "f"). + Group("severity"). + Scan(ctx, &results) + + return results, err +} + +func (s *Store) FetchNamespaceKinds(ctx context.Context, filter Filter) ([]string, error) { + list := make([]string, 0) + + err := NewFilterQuery(s.db, "resource_kind"). + FilterMap(map[string][]string{ + "f.source": filter.Sources, + "f.category": filter.Categories, + "f.resource_namespace": filter.Namespaces, + }). + Exclude(filter, "f"). + FilterReportLabels(filter.ReportLabel). + NamespaceScope(). + Scan(ctx, &list) + + return list, err +} + +func (s *Store) FetchClusterKinds(ctx context.Context, filter Filter) ([]string, error) { + list := make([]string, 0) + + err := NewFilterQuery(s.db, "resource_kind"). + FilterMap(map[string][]string{ + "f.source": filter.Sources, + "f.category": filter.Categories, + }). + Exclude(filter, "f"). + FilterReportLabels(filter.ReportLabel). + ClusterScope(). + Scan(ctx, &list) + + return list, err +} + +func (s *Store) FetchPolicies(ctx context.Context, filter Filter) ([]PolicyReportFilter, error) { + results := make([]PolicyReportFilter, 0) + + err := FromQuery(s.db.NewSelect().Model(&results).ColumnExpr("f.severity, f.category, f.policy, f.source, f.result, SUM(f.count) as count")). + FilterMap(map[string][]string{ + "f.source": filter.Sources, + "f.category": filter.Categories, + "f.resource_kind": filter.Kinds, + }). + PolicySearch(filter.Search). + Exclude(filter, "f"). + FilterReportLabels(filter.ReportLabel). + Order("f.source ASC", "f.category ASC"). + Group("f.category", "f.policy", "f.source", "f.result", "f.severity"). + Scan(ctx) + + return results, err +} + +func (s *Store) FetchFindingCounts(ctx context.Context, filter Filter) ([]StatusCount, error) { + results := make([]StatusCount, 0) + + query := FromQuery(s.db. + NewSelect(). + TableExpr("policy_report_filter as f"). + ColumnExpr("SUM(f.count) as count, f.result as status, f.source")) + + if filter.Namespaced { + query. + NamespaceScope(). + Filter("resource_namespace", filter.Namespaces) + } else { + query.FilterOptionalNamespaces(filter.Namespaces) + } + + err := query. + FilterMap(map[string][]string{ + "source": filter.Sources, + "category": filter.Categories, + "resource_kind": filter.Kinds, + "policy": filter.Policies, + "status": filter.Status, + }). + FilterReportLabels(filter.ReportLabel). + Exclude(filter, "f"). + Group("f.source", "status"). + Order("f.source"). + Scan(ctx, &results) + + return results, err +} + +func (s *Store) FetchSeverityFindingCounts(ctx context.Context, filter Filter) ([]SeverityCount, error) { + results := make([]SeverityCount, 0) + + query := FromQuery(s.db. + NewSelect(). + TableExpr("policy_report_filter as f"). + ColumnExpr("SUM(f.count) as count, f.severity, f.source")) + + if filter.Namespaced { + query. + NamespaceScope(). + Filter("resource_namespace", filter.Namespaces) + } else { + query.FilterOptionalNamespaces(filter.Namespaces) + } + + err := query. + FilterMap(map[string][]string{ + "source": filter.Sources, + "category": filter.Categories, + "resource_kind": filter.Kinds, + "policy": filter.Policies, + "severity": filter.Severities, + }). + FilterReportLabels(filter.ReportLabel). + Exclude(filter, "f"). + Group("f.source", "f.severity"). + Order("f.source"). + Scan(ctx, &results) + + return results, err +} + +///////////////////////// +/// Lifecycle Methods /// +///////////////////////// + func (s *Store) CreateSchemas(ctx context.Context) error { if s.db.Dialect().Name() == dialect.SQLite { if _, err := s.db.Exec("PRAGMA foreign_keys = ON"); err != nil { @@ -77,6 +895,14 @@ func (s *Store) CreateSchemas(ctx context.Context) error { Exec(ctx) logOnError("create policy_report_filter table", err) + _, err = s.db. + NewCreateTable(). + IfNotExists(). + Model((*ResourceResult)(nil)). + ForeignKey(`(policy_report_id) REFERENCES policy_report(id) ON DELETE CASCADE`). + Exec(ctx) + logOnError("create policy_report_resource table", err) + return err } @@ -105,6 +931,12 @@ func (s *Store) DropSchema(ctx context.Context) error { Exec(ctx) logOnError("drop policy_report table", err) + _, err = s.db.NewDropTable(). + IfExists(). + Model((*ResourceResult)(nil)). + Exec(ctx) + logOnError("drop policy_report_resource table", err) + return err } @@ -123,6 +955,15 @@ func (s *Store) Add(ctx context.Context, report v1alpha2.ReportInterface) error } } + resources := chunkSlice(MapPolicyReportResource(report), 50) + for _, list := range resources { + _, err = s.db.NewInsert().Model(&list).Exec(ctx) + if err != nil { + zap.L().Error("failed to bulk import policy report resources", zap.Error(err)) + return err + } + } + results := chunkSlice(MapPolicyReportResults(report), 50) for _, list := range results { _, err = s.db.NewInsert().Ignore().Model(&list).Exec(ctx) @@ -164,506 +1005,6 @@ func (s *Store) CleanUp(ctx context.Context) error { return err } -func (s *Store) FetchPolicyReports(ctx context.Context, filter api.Filter, pagination api.Pagination) ([]*api.PolicyReport, error) { - list := []*api.PolicyReport{} - query := s.db.NewSelect().Model((*PolicyReport)(nil)) - - s.addFilter(query, filter) - addPolicyReportFilter(query, filter) - query.Where(`pr.type = ?`, report.PolicyReportType) - - addPagination(query, pagination) - - err := query.Scan(ctx, &list) - if err != nil { - zap.L().Error("failed to select policy report results", zap.Error(err), zap.Any("filter", filter), zap.Any("pagination", pagination)) - } - - return list, err -} - -func (s *Store) CountPolicyReports(ctx context.Context, filter api.Filter) (int, error) { - query := s.db.NewSelect().Model((*PolicyReport)(nil)) - - s.addFilter(query, filter) - addPolicyReportFilter(query, filter) - query.Where(`pr.type = ?`, report.PolicyReportType) - - count, err := query.Count(ctx) - if err != nil { - zap.L().Error("failed to select policy report results", zap.Error(err), zap.Any("filter", filter)) - } - - return count, err -} - -func (s *Store) FetchNamespacedReportLabels(ctx context.Context, filter api.Filter) (map[string][]string, error) { - results := []string{} - list := make(map[string][]string) - - query := s.db.NewSelect(). - TableExpr("policy_report as pr"). - Distinct(). - Where(`pr.type = ?`, report.PolicyReportType) - - if s.db.Dialect().Name() == dialect.PG { - query.ColumnExpr("labels::text") - } else { - query.Column("labels") - } - - addPolicyReportFilter(query, filter) - - err := query.Scan(ctx, &results) - if err != nil { - return list, err - } - - for _, labels := range results { - for key, value := range convertJSONToMap(labels) { - _, ok := list[key] - contained := contains(value, list[key]) - - if ok && !contained { - list[key] = append(list[key], value) - continue - } else if ok && contained { - continue - } - - list[key] = []string{value} - } - } - - return list, nil -} - -func (s *Store) FetchClusterPolicyReports(ctx context.Context, filter api.Filter, pagination api.Pagination) ([]*api.PolicyReport, error) { - list := []*api.PolicyReport{} - query := s.db.NewSelect().Model((*PolicyReport)(nil)) - - s.addFilter(query, filter) - addPolicyReportFilter(query, filter) - query.Where(`pr.type = ?`, report.ClusterPolicyReportType) - - addPagination(query, pagination) - - err := query.Scan(ctx, &list) - if err != nil { - zap.L().Error("failed to select policy report results", zap.Error(err), zap.Any("filter", filter), zap.Any("pagination", pagination)) - } - - return list, err -} - -func (s *Store) CountClusterPolicyReports(ctx context.Context, filter api.Filter) (int, error) { - query := s.db.NewSelect().Model((*PolicyReport)(nil)) - - s.addFilter(query, filter) - addPolicyReportFilter(query, filter) - query.Where(`pr.type = ?`, report.ClusterPolicyReportType) - - count, err := query.Count(ctx) - if err != nil { - zap.L().Error("failed to select policy report results", zap.Error(err), zap.Any("filter", filter)) - } - - return count, err -} - -func (s *Store) FetchClusterReportLabels(ctx context.Context, filter api.Filter) (map[string][]string, error) { - results := []string{} - list := make(map[string][]string) - - query := s.db.NewSelect(). - TableExpr("policy_report as pr"). - Distinct(). - Where(`pr.type = ?`, report.ClusterPolicyReportType) - - if s.db.Dialect().Name() == dialect.PG { - query.ColumnExpr("labels::text") - } else { - query.Column("labels") - } - - addPolicyReportFilter(query, filter) - - err := query.Scan(ctx, &results) - if err != nil { - return list, err - } - - for _, labels := range results { - for key, value := range convertJSONToMap(labels) { - _, ok := list[key] - contained := contains(value, list[key]) - - if ok && !contained { - list[key] = append(list[key], value) - continue - } else if ok && contained { - continue - } - - list[key] = []string{value} - } - } - - return list, nil -} - -func (s *Store) FetchClusterRules(ctx context.Context, filter api.Filter) ([]string, error) { - list := make([]string, 0) - - query := s.db. - NewSelect(). - TableExpr("policy_report_result as r"). - Column("rule"). - Distinct(). - Order("rule ASC"). - Where(`r.resource_namespace = ''`) - - if len(filter.ReportLabel) > 0 { - query.Join("JOIN policy_report AS pr ON pr.id = r.policy_report_id") - } - - s.addFilter(query, filter) - addPolicyReportResultFilter(query, filter) - - query.Scan(ctx, &list) - - return list, nil -} - -func (s *Store) FetchClusterResources(ctx context.Context, filter api.Filter) ([]*api.Resource, error) { - list := make([]*api.Resource, 0) - - query := s.db. - NewSelect(). - TableExpr("policy_report_result as r"). - ColumnExpr("resource_name as name, resource_kind as kind"). - Distinct(). - Order("kind ASC", "name ASC"). - Where(`r.resource_namespace = ''`) - - if len(filter.ReportLabel) > 0 { - query.Join("JOIN policy_report AS pr ON pr.id = r.policy_report_id") - } - - s.addFilter(query, filter) - addPolicyReportResultFilter(query, filter) - - query.Scan(ctx, &list) - - return list, nil -} - -func (s *Store) FetchClusterPolicies(ctx context.Context, filter api.Filter) ([]string, error) { - return s.fetchFilterOptions(ctx, "policy", filter, false) -} - -func (s *Store) FetchClusterKinds(ctx context.Context, filter api.Filter) ([]string, error) { - return s.fetchFilterOptions(ctx, "kind", filter, false) -} - -func (s *Store) FetchClusterCategories(ctx context.Context, filter api.Filter) ([]string, error) { - return s.fetchFilterOptions(ctx, "category", filter, false) -} - -func (s *Store) FetchClusterSources(ctx context.Context) ([]string, error) { - return s.fetchFilterOptions(ctx, "source", api.Filter{}, false) -} - -func (s *Store) FetchClusterStatusCounts(ctx context.Context, filter api.Filter) ([]api.StatusCount, error) { - var list map[string]api.StatusCount - - if len(filter.Status) == 0 { - list = map[string]api.StatusCount{ - v1alpha2.StatusPass: {Status: v1alpha2.StatusPass}, - v1alpha2.StatusFail: {Status: v1alpha2.StatusFail}, - v1alpha2.StatusWarn: {Status: v1alpha2.StatusWarn}, - v1alpha2.StatusError: {Status: v1alpha2.StatusError}, - v1alpha2.StatusSkip: {Status: v1alpha2.StatusSkip}, - } - } else { - list = map[string]api.StatusCount{} - - for _, status := range filter.Status { - list[status] = api.StatusCount{Status: status} - } - } - - counts := make([]api.StatusCount, 0, len(list)) - results := make([]api.StatusCount, 0) - - query := s.db. - NewSelect(). - TableExpr("policy_report_filter as f"). - ColumnExpr("SUM(f.count) as count, f.result as status"). - Where(`f.namespace = ''`). - Group("status") - - if len(filter.ReportLabel) > 0 { - query.Join("JOIN policy_report AS pr ON pr.id = f.policy_report_id") - } - - s.addFilter(query, filter) - addPolicyReportFilterFilter(query, filter) - - err := query.Scan(ctx, &results) - if err != nil { - zap.L().Error("failed to load cluster status counts", zap.Error(err)) - return nil, err - } - - for _, count := range results { - list[count.Status] = count - } - - for _, count := range list { - counts = append(counts, count) - } - - return counts, nil -} - -func (s *Store) FetchClusterResults(ctx context.Context, filter api.Filter, pagination api.Pagination) ([]*api.ListResult, error) { - results := make([]*PolicyReportResult, 0) - - query := s.db. - NewSelect(). - Model(&results). - Where(`r.resource_namespace = ''`) - - if len(filter.ReportLabel) > 0 { - query.Join("JOIN policy_report AS pr ON pr.id = r.policy_report_id") - } - - s.addFilter(query, filter) - addPolicyReportResultFilter(query, filter) - addPagination(query, pagination) - - err := query.Scan(ctx) - if err != nil { - return nil, err - } - - return MapListResult(results), nil -} - -func (s *Store) CountClusterResults(ctx context.Context, filter api.Filter) (int, error) { - query := s.db. - NewSelect(). - Model((*PolicyReportResult)(nil)). - Where(`r.resource_namespace = ''`) - - if len(filter.ReportLabel) > 0 { - query.Join("JOIN policy_report AS pr ON pr.id = r.policy_report_id") - } - - s.addFilter(query, filter) - addPolicyReportResultFilter(query, filter) - - return query.Count(ctx) -} - -func (s *Store) FetchNamespacedRules(ctx context.Context, filter api.Filter) ([]string, error) { - list := make([]string, 0) - - query := s.db. - NewSelect(). - TableExpr("policy_report_result as r"). - Column("rule"). - Distinct(). - Order("rule ASC"). - Where(`r.resource_namespace != ''`) - - if len(filter.ReportLabel) > 0 { - query.Join("JOIN policy_report AS pr ON pr.id = r.policy_report_id") - } - - s.addFilter(query, filter) - addPolicyReportResultFilter(query, filter) - - query.Scan(ctx, &list) - - return list, nil -} - -func (s *Store) FetchNamespacedResources(ctx context.Context, filter api.Filter) ([]*api.Resource, error) { - list := make([]*api.Resource, 0) - - query := s.db. - NewSelect(). - TableExpr("policy_report_result as r"). - ColumnExpr("resource_name as name, resource_kind as kind"). - Distinct(). - Order("kind ASC", "name ASC"). - Where(`r.resource_namespace != ''`) - - if len(filter.ReportLabel) > 0 { - query.Join("JOIN policy_report AS pr ON pr.id = r.policy_report_id") - } - - s.addFilter(query, filter) - addPolicyReportResultFilter(query, filter) - - query.Scan(ctx, &list) - - return list, nil -} - -func (s *Store) FetchNamespacedPolicies(ctx context.Context, filter api.Filter) ([]string, error) { - return s.fetchFilterOptions(ctx, "policy", filter, true) -} - -func (s *Store) FetchNamespacedKinds(ctx context.Context, filter api.Filter) ([]string, error) { - return s.fetchFilterOptions(ctx, "kind", filter, true) -} - -func (s *Store) FetchNamespacedCategories(ctx context.Context, filter api.Filter) ([]string, error) { - return s.fetchFilterOptions(ctx, "category", filter, true) -} - -func (s *Store) FetchNamespacedSources(ctx context.Context) ([]string, error) { - return s.fetchFilterOptions(ctx, "source", api.Filter{}, true) -} - -func (s *Store) FetchNamespaces(ctx context.Context, filter api.Filter) ([]string, error) { - return s.fetchFilterOptions(ctx, "f.namespace", filter, true) -} - -func (s *Store) FetchNamespacedStatusCounts(ctx context.Context, filter api.Filter) ([]api.NamespacedStatusCount, error) { - var list map[string][]api.NamespaceCount - - if len(filter.Status) == 0 { - list = map[string][]api.NamespaceCount{ - v1alpha2.StatusPass: make([]api.NamespaceCount, 0), - v1alpha2.StatusFail: make([]api.NamespaceCount, 0), - v1alpha2.StatusWarn: make([]api.NamespaceCount, 0), - v1alpha2.StatusError: make([]api.NamespaceCount, 0), - v1alpha2.StatusSkip: make([]api.NamespaceCount, 0), - } - } else { - list = map[string][]api.NamespaceCount{} - - for _, status := range filter.Status { - list[status] = make([]api.NamespaceCount, 0) - } - } - - statusCounts := make([]api.NamespacedStatusCount, 0, 5) - counts := make([]api.NamespaceCount, 0) - - query := s.db. - NewSelect(). - TableExpr("policy_report_filter as f"). - ColumnExpr("SUM(f.count) as count, f.namespace, f.result as status"). - Where(`f.namespace != ''`). - Group("f.namespace", "status"). - Order("f.namespace ASC") - - if len(filter.ReportLabel) > 0 { - query.Join("JOIN policy_report AS pr ON pr.id = f.policy_report_id") - } - - s.addFilter(query, filter) - addPolicyReportFilterFilter(query, filter) - - err := query.Scan(ctx, &counts) - if err != nil { - zap.L().Error("failed to load namespaced status counts", zap.Error(err)) - return nil, err - } - - for _, count := range counts { - list[count.Status] = append(list[count.Status], count) - } - - for status, items := range list { - statusCounts = append(statusCounts, api.NamespacedStatusCount{ - Status: status, - Items: items, - }) - } - - return statusCounts, nil -} - -func (s *Store) FetchRuleStatusCounts(ctx context.Context, policy, rule string) ([]api.StatusCount, error) { - list := map[string]api.StatusCount{ - v1alpha2.StatusPass: {Status: v1alpha2.StatusPass}, - v1alpha2.StatusFail: {Status: v1alpha2.StatusFail}, - v1alpha2.StatusWarn: {Status: v1alpha2.StatusWarn}, - v1alpha2.StatusError: {Status: v1alpha2.StatusError}, - v1alpha2.StatusSkip: {Status: v1alpha2.StatusSkip}, - } - - statusCounts := make([]api.StatusCount, 0, len(list)) - counts := make([]api.StatusCount, 0) - - err := s.db.NewSelect(). - Table("policy_report_result"). - ColumnExpr("COUNT(id) as count, result as status"). - Where("rule = ?", rule). - Where("policy = ?", policy). - Group("status"). - Scan(ctx, &counts) - if err != nil { - return statusCounts, err - } - - for _, count := range counts { - list[count.Status] = count - } - - for _, count := range list { - statusCounts = append(statusCounts, count) - } - - return statusCounts, nil -} - -func (s *Store) FetchNamespacedResults(ctx context.Context, filter api.Filter, pagination api.Pagination) ([]*api.ListResult, error) { - results := make([]*PolicyReportResult, 0) - - query := s.db. - NewSelect(). - Model(&results). - Where(`r.resource_namespace != ''`) - - if len(filter.ReportLabel) > 0 { - query.Join("JOIN policy_report AS pr ON pr.id = r.policy_report_id") - } - - s.addFilter(query, filter) - addPolicyReportResultFilter(query, filter) - addPagination(query, pagination) - - err := query.Scan(ctx) - if err != nil { - return nil, err - } - - return MapListResult(results), nil -} - -func (s *Store) CountNamespacedResults(ctx context.Context, filter api.Filter) (int, error) { - query := s.db. - NewSelect(). - Model((*PolicyReportResult)(nil)). - Where(`r.resource_namespace != ''`) - - if len(filter.ReportLabel) > 0 { - query.Join("JOIN policy_report AS pr ON pr.id = r.policy_report_id") - } - - s.addFilter(query, filter) - addPolicyReportResultFilter(query, filter) - - return query.Count(ctx) -} - func (s *Store) Get(ctx context.Context, id string) (v1alpha2.ReportInterface, error) { polr := &PolicyReport{} @@ -747,45 +1088,11 @@ func (s *Store) fetchResults(ctx context.Context, id string) ([]v1alpha2.PolicyR return list, nil } -func (s *Store) fetchFilterOptions(ctx context.Context, option string, filter api.Filter, namespaced bool) ([]string, error) { - list := make([]string, 0) - - query := s.db. - NewSelect(). - TableExpr("policy_report_filter as f"). - Column(option). - Distinct(). - Order(option+" ASC"). - Where(`? != ''`, bun.Ident(option)) - - if namespaced { - query.Where(`f.namespace != ''`) - } else { - query.Where(`f.namespace = ''`) - } - - if len(filter.ReportLabel) > 0 { - query.Join("JOIN policy_report AS pr ON pr.id = f.policy_report_id") - } - - s.addFilter(query, filter) - addPolicyReportFilterFilter(query, filter) - - err := query.Scan(ctx, &list) - - return list, err -} - -func (s *Store) Configure() { - if s.db.Dialect().Name() == dialect.PG { - s.jsonExtractLayout = "(pr.labels->>'%s') = ?" - return - } - - s.jsonExtractLayout = "json_extract(pr.labels, '$.\"%s\"') = ?" -} - func (s *Store) RequireSchemaUpgrade(ctx context.Context) bool { + if s.IsSQLite() { + return true + } + config := Config{} err := s.db.NewSelect().Model(&config).Where("id = ?", 1).Scan(ctx) @@ -841,8 +1148,6 @@ func NewStore(db *bun.DB, version string) (*Store, error) { version: version, } - s.Configure() - return s, nil } @@ -872,106 +1177,6 @@ func createSQLiteDB(dbFile string) (*sql.DB, error) { return db, nil } -func addPolicyReportFilterFilter(query *bun.SelectQuery, filter api.Filter) { - if len(filter.Namespaces) > 0 { - query.Where("f.namespace IN (?)", bun.In(filter.Namespaces)) - } - if len(filter.Kinds) > 0 { - query.Where("f.kind IN (?)", bun.In(filter.Kinds)) - } - if len(filter.Sources) > 0 { - query.Where("f.source IN (?)", bun.In(filter.Sources)) - } -} - -func addPolicyReportResultFilter(query *bun.SelectQuery, filter api.Filter) { - if len(filter.Namespaces) > 0 { - query.Where("r.resource_namespace IN (?)", bun.In(filter.Namespaces)) - } - if len(filter.Rules) > 0 { - query.Where("r.rule IN (?)", bun.In(filter.Rules)) - } - if len(filter.Kinds) > 0 { - query.Where("r.resource_kind IN (?)", bun.In(filter.Kinds)) - } - if len(filter.Resources) > 0 { - query.Where("r.resource_name IN (?)", bun.In(filter.Resources)) - } - if len(filter.Sources) > 0 { - query.Where("r.source IN (?)", bun.In(filter.Sources)) - } - - if filter.Search != "" { - query.Where(`(resource_namespace LIKE ?0 OR resource_name LIKE ?0 OR policy LIKE ?0 OR rule LIKE ?0 OR severity = ?1 OR result = ?1 OR LOWER(resource_kind) = LOWER(?1))`, "%"+filter.Search+"%", filter.Search) - } -} - -func addPolicyReportFilter(query *bun.SelectQuery, filter api.Filter) { - if len(filter.Namespaces) > 0 { - query.Where("pr.namespace IN (?)", bun.In(filter.Namespaces)) - } - if len(filter.Sources) > 0 { - query.Where("pr.source IN (?)", bun.In(filter.Sources)) - } -} - -func (s *Store) addFilter(query *bun.SelectQuery, filter api.Filter) { - if len(filter.Policies) > 0 { - query.Where("policy IN (?)", bun.In(filter.Policies)) - } - if len(filter.Categories) > 0 { - query.Where("category IN (?)", bun.In(filter.Categories)) - } - if len(filter.Severities) > 0 { - query.Where("severity IN (?)", bun.In(filter.Severities)) - } - if len(filter.Status) > 0 { - query.Where("result IN (?)", bun.In(filter.Status)) - } - - if len(filter.ReportLabel) > 0 { - for key, value := range filter.ReportLabel { - query.Where(fmt.Sprintf(s.jsonExtractLayout, key), value) - } - } -} - -func addPagination(query *bun.SelectQuery, pagination api.Pagination) { - query.OrderExpr(fmt.Sprintf( - "%s %s", - strings.Join(pagination.SortBy, ","), - pagination.Direction, - )) - - if pagination.Page == 0 || pagination.Offset == 0 { - return - } - - query.Limit(pagination.Offset) - query.Offset((pagination.Page - 1) * pagination.Offset) -} - -func convertJSONToMap(s string) map[string]string { - m := make(map[string]string) - if s == "" { - return m - } - - _ = json.Unmarshal([]byte(s), &m) - - return m -} - -func contains(source string, sources []string) bool { - for _, s := range sources { - if strings.EqualFold(s, source) { - return true - } - } - - return false -} - func chunkSlice[K interface{}](slice []K, chunkSize int) [][]K { var chunks [][]K for i := 0; i < len(slice); i += chunkSize { diff --git a/pkg/database/bun_test.go b/pkg/database/bun_test.go deleted file mode 100644 index af6bcff4..00000000 --- a/pkg/database/bun_test.go +++ /dev/null @@ -1,678 +0,0 @@ -package database_test - -import ( - "context" - "database/sql" - "testing" - - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - v1 "github.com/kyverno/policy-reporter/pkg/api/v1" - "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" - "github.com/kyverno/policy-reporter/pkg/database" - "github.com/kyverno/policy-reporter/pkg/fixtures" -) - -var pagination = v1.Pagination{Page: 1, Offset: 20, Direction: "ASC", SortBy: []string{"resource_name"}} - -var polrPagination = v1.Pagination{Page: 1, Offset: 20, Direction: "ASC", SortBy: []string{"namespace"}} - -var preport = &v1alpha2.PolicyReport{ - ObjectMeta: metav1.ObjectMeta{ - Name: "polr-test", - Namespace: "test", - Labels: map[string]string{"app": "policy-reporter", "scope": "namespaced"}, - CreationTimestamp: metav1.Now(), - }, - Results: []v1alpha2.PolicyReportResult{fixtures.FailResult}, - Summary: v1alpha2.PolicyReportSummary{Fail: 1}, -} - -var dreport = &v1alpha2.PolicyReport{ - ObjectMeta: metav1.ObjectMeta{ - Name: "polr-test", - Namespace: "test", - Labels: map[string]string{"app": "policy-reporter", "scope": "namespaced"}, - CreationTimestamp: metav1.Now(), - }, - Results: []v1alpha2.PolicyReportResult{fixtures.FailResult, fixtures.FailResult, fixtures.FailPodResult}, - Summary: v1alpha2.PolicyReportSummary{Fail: 1}, -} - -var ureport = &v1alpha2.PolicyReport{ - ObjectMeta: metav1.ObjectMeta{ - Name: "polr-test", - Namespace: "test", - Labels: map[string]string{"app": "policy-reporter", "owner": "team-a", "scope": "namespaced"}, - CreationTimestamp: metav1.Now(), - }, - Results: []v1alpha2.PolicyReportResult{fixtures.FailResult, fixtures.PassPodResult}, - Summary: v1alpha2.PolicyReportSummary{Fail: 1, Pass: 1}, -} - -var creport = &v1alpha2.ClusterPolicyReport{ - ObjectMeta: metav1.ObjectMeta{ - Name: "cpolr", - Labels: map[string]string{"app": "policy-reporter", "scope": "cluster"}, - CreationTimestamp: metav1.Now(), - }, - Results: []v1alpha2.PolicyReportResult{fixtures.PassNamespaceResult, fixtures.FailNamespaceResult}, - Summary: v1alpha2.PolicyReportSummary{}, -} - -var scopeReport = &v1alpha2.PolicyReport{ - ObjectMeta: metav1.ObjectMeta{ - Name: "polr-scope-test", - Namespace: "test", - CreationTimestamp: metav1.Now(), - }, - Results: []v1alpha2.PolicyReportResult{fixtures.ScopeResult}, - Summary: v1alpha2.PolicyReportSummary{Fail: 1, Pass: 0}, - Scope: &corev1.ObjectReference{ - APIVersion: "v1", - Kind: "Deployment", - Name: "nginx", - Namespace: "test", - UID: "536ab69f-1b3c-4bd9-9ba4-274a56188409", - }, -} - -func Test_PolicyReportStore(t *testing.T) { - db, err := database.NewSQLiteDB("test.db") - if err != nil { - t.Fatal(err) - } - defer db.Close() - - ctx := context.Background() - - store, _ := database.NewStore(db, "develop") - store.PrepareDatabase(ctx) - - t.Run("Add/Get/Update PolicyReport", func(t *testing.T) { - _, err := store.Get(ctx, preport.GetID()) - if err != sql.ErrNoRows { - t.Fatalf("Should not be found in empty Store") - } - - err = store.Add(ctx, preport) - if err != nil { - t.Fatalf("Unexpected error: %s", err) - } - - polr, err := store.Get(ctx, preport.GetID()) - if err != nil { - t.Fatalf("Should found policy reporter after adding: %v", err) - } - - if len(polr.GetResults()) == 0 { - t.Fatalf("Failed to load PolicyReportResults: %v", err) - } - - err = store.Update(ctx, ureport) - if err != nil { - t.Fatalf("Failed to update policy report: %v", err) - } - - r2, _ := store.Get(ctx, ureport.GetID()) - if r2.GetSummary().Pass != 1 { - t.Errorf("Expected 1 Passed Results in GetSummary() after Update") - } - - if r2.GetLabels()["owner"] != "team-a" { - t.Errorf("Expected Labels are updated") - } - }) - - t.Run("Add/Get PolicyReport with ScopeResource", func(t *testing.T) { - _, err := store.Get(ctx, scopeReport.GetID()) - if err != sql.ErrNoRows { - t.Fatalf("Should not be found in empty Store") - } - - err = store.Add(ctx, scopeReport) - if err != nil { - t.Fatalf("Unexpected add error: %s", err) - } - - rep, err := store.Get(ctx, scopeReport.GetID()) - if err != nil { - t.Error("Should be found in Store after adding report to the store") - } - if len(rep.GetResults()) == 0 { - t.Fatal("Exptected at least one result on the report") - } - res := rep.GetResults()[0] - if !res.HasResource() { - t.Error("Expected scope resource set as result resource") - } - - store.Remove(ctx, rep.GetID()) - }) - - t.Run("Add/Get ClusterPolicyReport", func(t *testing.T) { - _, err := store.Get(ctx, creport.GetID()) - if err != sql.ErrNoRows { - t.Fatalf("Should not be found in empty Store") - } - - err = store.Add(ctx, creport) - if err != nil { - t.Fatalf("Failed to persist ClusterPolicyReport: %v", err) - } - - _, err = store.Get(ctx, creport.GetID()) - if err != nil { - t.Fatalf("Should be found in Store after adding report to the store") - } - }) - - t.Run("FetchPolicyReports", func(t *testing.T) { - items, err := store.FetchPolicyReports(ctx, v1.Filter{Namespaces: []string{"test"}, ReportLabel: map[string]string{"scope": "namespaced"}}, polrPagination) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - - if len(items) != 1 { - t.Fatalf("Should return one policy report, got %d", len(items)) - } - }) - - t.Run("CountPolicyReports", func(t *testing.T) { - count, err := store.CountPolicyReports(ctx, v1.Filter{Namespaces: []string{"test"}, ReportLabel: map[string]string{"scope": "namespaced"}}) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - - if count != 1 { - t.Fatalf("Should return one policy report, got %d", count) - } - }) - - t.Run("NamespacedGetLabels()", func(t *testing.T) { - items, err := store.FetchNamespacedReportLabels(ctx, v1.Filter{Sources: []string{"Kyverno"}, Namespaces: []string{"test"}}) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - - if len(items) != 3 { - t.Fatalf("Should return 3 GetLabels() results") - } - - if len(items["scope"]) != 1 && items["scope"][0] != "namespaced" { - t.Fatalf("Should return cluster as scope value") - } - - if len(items["app"]) != 1 && items["app"][0] != "policy-reporter" { - t.Fatalf("Should return policy-reporter as app value") - } - - if len(items["owner"]) != 1 && items["owner"][0] != "team-a" { - t.Fatalf("Should return policy-reporter as app value") - } - }) - t.Run("FetchClusterReports", func(t *testing.T) { - items, err := store.FetchClusterPolicyReports(ctx, v1.Filter{ReportLabel: map[string]string{"scope": "cluster"}}, polrPagination) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - - if len(items) != 1 { - t.Fatalf("Should return one policy report, got %d", len(items)) - } - }) - - t.Run("CountClusterReports", func(t *testing.T) { - items, err := store.CountClusterPolicyReports(ctx, v1.Filter{ReportLabel: map[string]string{"scope": "cluster"}}) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - - if items != 1 { - t.Fatalf("Should return one policy report, got %d", items) - } - }) - - t.Run("ClusterGetLabels()", func(t *testing.T) { - items, err := store.FetchClusterReportLabels(ctx, v1.Filter{Sources: []string{"Kyverno"}}) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - - if len(items) != 2 { - t.Fatalf("Should return 2 GetLabels() results") - } - - if len(items["scope"]) != 1 && items["scope"][0] != "cluster" { - t.Fatalf("Should return cluster as scope value") - } - - if len(items["app"]) != 1 && items["app"][0] != "policy-reporter" { - t.Fatalf("Should return policy-reporter as app value") - } - }) - - t.Run("FetchClusterPolicies", func(t *testing.T) { - items, err := store.FetchClusterPolicies(ctx, v1.Filter{Sources: []string{"Kyverno"}, ReportLabel: map[string]string{"app": "policy-reporter"}}) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - if len(items) != 1 { - t.Fatalf("Should Find 1 cluster scoped policy, found %d", len(items)) - } - if items[0] != "require-ns-GetLabels()" { - t.Fatalf("Should return 'require-ns-GetLabels()' policy") - } - }) - - t.Run("FetchClusterRules", func(t *testing.T) { - items, err := store.FetchClusterRules(ctx, v1.Filter{Sources: []string{"Kyverno"}, ReportLabel: map[string]string{"app": "policy-reporter"}}) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - if len(items) != 1 { - t.Fatalf("Should Find 1 cluster scoped rule, found %d", len(items)) - } - if items[0] != "check-for-GetLabels()-on-namespace" { - t.Fatalf("Should return 'check-for-GetLabels()-on-namespace' rule") - } - }) - - t.Run("FetchNamespacedPolicies", func(t *testing.T) { - items, err := store.FetchNamespacedPolicies(ctx, v1.Filter{Sources: []string{"Kyverno"}, ReportLabel: map[string]string{"app": "policy-reporter"}}) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - if len(items) != 1 { - t.Errorf("Should find 1 namespace scoped policy") - } - if items[0] != "require-requests-and-limits-required" { - t.Errorf("Should return 'require-requests-and-limits-required' policy") - } - }) - - t.Run("FetchNamespacedRules", func(t *testing.T) { - items, err := store.FetchNamespacedRules(ctx, v1.Filter{Sources: []string{"Kyverno"}, ReportLabel: map[string]string{"app": "policy-reporter"}}) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - if len(items) != 1 { - t.Fatalf("Should find 1 namespace scoped policy, found %d", len(items)) - } - if items[0] != "autogen-check-for-requests-and-limits" { - t.Fatalf("Should return 'require-requests-and-limits-required' policy") - } - }) - - t.Run("FetchNamespacedResources", func(t *testing.T) { - items, err := store.FetchNamespacedResources(ctx, v1.Filter{Sources: []string{"Kyverno"}, Kinds: []string{"Pod"}, ReportLabel: map[string]string{"app": "policy-reporter"}}) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - if len(items) != 1 { - t.Fatalf("Should find 1 distinct resource with namespace Scope, got %d", len(items)) - } - if items[0].Name != "nginx" { - t.Errorf("Should return 'nginx' as first result, got %s", items[0].Name) - } - }) - - t.Run("FetchClusterResources", func(t *testing.T) { - items, err := store.FetchClusterResources(ctx, v1.Filter{Sources: []string{"Kyverno"}, Kinds: []string{"Namespace"}, ReportLabel: map[string]string{"app": "policy-reporter"}}) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - if len(items) != 2 { - t.Fatalf("Should find 2 resources with cluster scope") - } - if items[0].Name != "dev" { - t.Errorf("Should return 'test' as first result") - } - if items[1].Name != "test" { - t.Errorf("Should return 'test' as second result") - } - }) - - t.Run("FetchClusterSources", func(t *testing.T) { - items, err := store.FetchClusterSources(ctx) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - if len(items) != 1 { - t.Errorf("Should find 1 Source") - } - if items[0] != "Kyverno" { - t.Errorf("Should return Kyverno") - } - }) - - t.Run("FetchNamespacedSources", func(t *testing.T) { - items, err := store.FetchNamespacedSources(ctx) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - if len(items) != 1 { - t.Errorf("Should find 1 Source") - } - if items[0] != "Kyverno" { - t.Errorf("Should return Kyverno") - } - }) - - t.Run("FetchNamespaces", func(t *testing.T) { - items, err := store.FetchNamespaces(ctx, v1.Filter{Sources: []string{"Kyverno"}, ReportLabel: map[string]string{"app": "policy-reporter"}}) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - if len(items) != 1 { - t.Fatal("Should find 1 Namespace") - } - if items[0] != "test" { - t.Errorf("Should return test namespace") - } - }) - - t.Run("FetchNamespacedStatusCounts", func(t *testing.T) { - items, err := store.FetchNamespacedStatusCounts(ctx, v1.Filter{ReportLabel: map[string]string{"app": "policy-reporter"}}) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - if len(items) != 5 { - t.Fatalf("Should include 1 item per possible status") - } - - var passed v1.NamespacedStatusCount - var failed v1.NamespacedStatusCount - for _, item := range items { - if item.Status == v1alpha2.StatusPass { - passed = item - } - if item.Status == v1alpha2.StatusFail { - failed = item - } - } - - if passed.Status != v1alpha2.StatusPass { - t.Errorf("Expected Pass Counts as first item") - } - if passed.Items[0].Count != 1 { - t.Errorf("Expected count to be one for pass") - } - - if failed.Status != v1alpha2.StatusFail { - t.Errorf("Expected Pass Counts as first item") - } - if failed.Items[0].Count != 1 { - t.Errorf("Expected count to be one for fail") - } - }) - - t.Run("FetchNamespacedStatusCounts with StatusFilter", func(t *testing.T) { - items, err := store.FetchNamespacedStatusCounts(ctx, v1.Filter{Status: []string{v1alpha2.StatusPass}, ReportLabel: map[string]string{"app": "policy-reporter"}}) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - if len(items) != 1 { - t.Fatalf("Should have only 1 item for pass counts") - } - if items[0].Status != v1alpha2.StatusPass { - t.Errorf("Expected Pass Counts") - } - if items[0].Items[0].Count != 1 { - t.Errorf("Expected count to be one for pass") - } - }) - - t.Run("FetchClusterStatusCounts", func(t *testing.T) { - items, err := store.FetchClusterStatusCounts(ctx, v1.Filter{ReportLabel: map[string]string{"app": "policy-reporter"}}) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - var passed v1.StatusCount - var failed v1.StatusCount - for _, item := range items { - if item.Status == v1alpha2.StatusPass { - passed = item - } - if item.Status == v1alpha2.StatusFail { - failed = item - } - } - if len(items) != 5 { - t.Fatalf("Should include 1 item per possible status") - } - if passed.Count != 1 { - t.Errorf("Expected count to be one for pass") - } - if failed.Count != 1 { - t.Errorf("Expected count to be one for fail") - } - }) - - t.Run("FetchClusterStatusCounts with StatusFilter", func(t *testing.T) { - items, err := store.FetchClusterStatusCounts(ctx, v1.Filter{Status: []string{v1alpha2.StatusPass}}) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - if len(items) != 1 { - t.Fatalf("Should have only 1 item for pass counts") - } - if items[0].Status != v1alpha2.StatusPass { - t.Errorf("Expected Pass Counts") - } - if items[0].Count != 1 { - t.Errorf("Expected count to be one for pass") - } - }) - - t.Run("FetchRuleStatusCounts", func(t *testing.T) { - items, err := store.FetchRuleStatusCounts(ctx, "require-requests-and-limits-required", "autogen-check-for-requests-and-limits") - var passed v1.StatusCount - var failed v1.StatusCount - for _, item := range items { - if item.Status == v1alpha2.StatusPass { - passed = item - } - if item.Status == v1alpha2.StatusFail { - failed = item - } - } - - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - if passed.Count != 1 { - t.Errorf("Expected count to be one for pass") - } - - if failed.Count != 1 { - t.Errorf("Expected count to be one for fail") - } - }) - - t.Run("FetchNamespacedResults", func(t *testing.T) { - items, err := store.FetchNamespacedResults(ctx, v1.Filter{Namespaces: []string{"test"}}, pagination) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - - if len(items) != 2 { - t.Fatalf("Should return 2 namespaced results") - } - }) - - t.Run("FetchNamespacedResults with SeverityFilter", func(t *testing.T) { - items, err := store.FetchNamespacedResults(ctx, v1.Filter{Severities: []string{v1alpha2.SeverityHigh}}, pagination) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - - if len(items) != 1 { - t.Fatalf("Should return 1 namespaced result") - } - if items[0].Severity != v1alpha2.SeverityHigh { - t.Fatalf("result with severity high") - } - }) - - t.Run("CountNamespacedResults", func(t *testing.T) { - count, err := store.CountNamespacedResults(ctx, v1.Filter{ReportLabel: map[string]string{"app": "policy-reporter"}}) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - - if count != 2 { - t.Fatalf("Should return 2 namespaced result") - } - }) - - t.Run("CountNamespacedResults with SeverityFilter", func(t *testing.T) { - count, err := store.CountNamespacedResults(ctx, v1.Filter{Severities: []string{v1alpha2.SeverityHigh}}) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - - if count != 1 { - t.Fatalf("Should return 1 namespaced result") - } - }) - - t.Run("FetchNamespacedResults with SearchFilter::Severity", func(t *testing.T) { - items, err := store.FetchNamespacedResults(ctx, v1.Filter{Search: v1alpha2.SeverityHigh}, pagination) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - - if len(items) != 1 { - t.Fatalf("Should return 1 namespaced result") - } - if items[0].Severity != v1alpha2.SeverityHigh { - t.Fatalf("result with severity high expected") - } - }) - - t.Run("FetchNamespacedResults with SearchFilter::Kind", func(t *testing.T) { - items, err := store.FetchNamespacedResults(ctx, v1.Filter{Search: "deployment"}, pagination) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - - if len(items) != 1 { - t.Fatalf("Should return 1 namespaced result, got %d", len(items)) - } - if items[0].Kind != "Deployment" { - t.Fatalf("result with kind Deployment expected") - } - }) - - t.Run("FetchClusterResults", func(t *testing.T) { - items, err := store.FetchClusterResults(ctx, v1.Filter{Status: []string{v1alpha2.StatusPass, v1alpha2.StatusFail}, ReportLabel: map[string]string{"app": "policy-reporter"}}, pagination) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - - if len(items) != 2 { - t.Fatalf("Should return 2 cluster results") - } - }) - - t.Run("CountClusterResults", func(t *testing.T) { - count, err := store.CountClusterResults(ctx, v1.Filter{Status: []string{v1alpha2.StatusPass, v1alpha2.StatusFail}, ReportLabel: map[string]string{"app": "policy-reporter"}}) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - - if count != 2 { - t.Fatalf("Should return 2 cluster results") - } - }) - - t.Run("FetchClusterResults with SeverityFilter", func(t *testing.T) { - items, err := store.FetchClusterResults(ctx, v1.Filter{Severities: []string{v1alpha2.SeverityHigh}}, pagination) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - - if len(items) != 1 { - t.Fatalf("Should return 1 namespaced result") - } - if items[0].Severity != v1alpha2.SeverityHigh { - t.Fatalf("result with severity high") - } - }) - - t.Run("FetchClusterResults with SearchFilter", func(t *testing.T) { - items, err := store.FetchClusterResults(ctx, v1.Filter{Search: v1alpha2.SeverityHigh}, pagination) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - - if len(items) != 1 { - t.Fatalf("Should return 1 namespaced result") - } - if items[0].Severity != v1alpha2.SeverityHigh { - t.Fatalf("result with severity high") - } - }) - - t.Run("FetchNamespacedKinds", func(t *testing.T) { - items, err := store.FetchNamespacedKinds(ctx, v1.Filter{Sources: []string{"Kyverno"}, ReportLabel: map[string]string{"app": "policy-reporter"}}) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - if len(items) != 2 { - t.Fatalf("Should Find 2 Kinds with Namespace Scope") - } - if items[0] != "Deployment" { - t.Errorf("Should return 'Deployment' as first result") - } - if items[1] != "Pod" { - t.Errorf("Should return 'Pod' as second result") - } - }) - - t.Run("FetchClusterKinds", func(t *testing.T) { - items, err := store.FetchClusterKinds(ctx, v1.Filter{Sources: []string{"Kyverno"}, ReportLabel: map[string]string{"app": "policy-reporter"}}) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - if len(items) != 1 { - t.Fatalf("Should find 1 kind with cluster scope") - } - if items[0] != "Namespace" { - t.Errorf("Should return 'Namespace' as first result") - } - }) - - t.Run("FetchNamespacedCategories", func(t *testing.T) { - items, err := store.FetchNamespacedCategories(ctx, v1.Filter{Sources: []string{"Kyverno"}, ReportLabel: map[string]string{"app": "policy-reporter"}}) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - if len(items) != 2 { - t.Errorf("Should find 2 categories, got %d", len(items)) - } - if items[0] != "Best Practices" { - t.Errorf("Should return 'Best Practices' as first category") - } - }) - - t.Run("FetchClusterCategories", func(t *testing.T) { - items, err := store.FetchClusterCategories(ctx, v1.Filter{Sources: []string{"Kyverno"}, ReportLabel: map[string]string{"app": "policy-reporter"}}) - if err != nil { - t.Fatalf("Unexpected Error: %s", err) - } - if len(items) != 1 { - t.Errorf("Should find 1 category, got %d", len(items)) - } - if items[0] != "namespaces" { - t.Errorf("Should return 'Best Practices' as first category, get '%s'", items[0]) - } - }) - - err = store.CleanUp(ctx) - if err != nil { - t.Fatalf("Failed to cleanup policy reports: %v", err) - } -} diff --git a/pkg/database/model.go b/pkg/database/model.go index 4e195721..000e8a1b 100644 --- a/pkg/database/model.go +++ b/pkg/database/model.go @@ -6,8 +6,8 @@ import ( "github.com/segmentio/fasthash/fnv1a" "github.com/uptrace/bun" - api "github.com/kyverno/policy-reporter/pkg/api/v1" "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" + "github.com/kyverno/policy-reporter/pkg/helper" "github.com/kyverno/policy-reporter/pkg/report" "github.com/kyverno/policy-reporter/pkg/report/result" ) @@ -44,11 +44,22 @@ type Resource struct { UID string } +func (r Resource) GetID() string { + h1 := fnv1a.Init64 + h1 = fnv1a.AddString64(h1, r.Namespace) + h1 = fnv1a.AddString64(h1, r.Name) + h1 = fnv1a.AddString64(h1, r.Kind) + h1 = fnv1a.AddString64(h1, r.APIVersion) + + return strconv.FormatUint(h1, 10) +} + type PolicyReportResult struct { bun.BaseModel `bun:"table:policy_report_result,alias:r" json:"-"` ID string `bun:",pk" json:"id"` - PolicyReportID string `bund:"policy_report_id" json:"-"` + PolicyReportID string `bun:"policy_report_id" json:"-"` + ResourceID string `bun:"resource_id"` Resource Resource `bun:"embed:resource_"` Policy string Rule string @@ -62,13 +73,34 @@ type PolicyReportResult struct { Created int64 } +type ResourceResult struct { + bun.BaseModel `bun:"table:policy_report_resource,alias:res" json:"-"` + + ID string `bun:",pk"` + PolicyReportID string `bun:"policy_report_id,pk"` + Resource Resource `bun:"embed:resource_"` + Source string `bun:",pk"` + Category string `bun:"category,pk"` + Pass int + Warn int + Fail int + Error int + Skip int + Info int + Low int + Medium int + High int + Critical int + Unknown int +} + type PolicyReportFilter struct { bun.BaseModel `bun:"table:policy_report_filter,alias:f"` - PolicyReportID string `bund:"policy_report_id"` - Namespace string + PolicyReportID string `bun:"policy_report_id"` + Namespace string `bun:"resource_namespace"` + Kind string `bun:"resource_kind"` Policy string - Kind string Result string Severity string Category string @@ -117,26 +149,29 @@ func MapPolicyReportResults(polr v1alpha2.ReportInterface) []*PolicyReportResult ns = polr.GetNamespace() } + resource := Resource{ + APIVersion: res.APIVersion, + Kind: res.Kind, + Name: res.Name, + Namespace: ns, + UID: string(res.UID), + } + list = append(list, &PolicyReportResult{ ID: r.GetID(), PolicyReportID: polr.GetID(), - Resource: Resource{ - APIVersion: res.APIVersion, - Kind: res.Kind, - Name: res.Name, - Namespace: ns, - UID: string(res.UID), - }, - Policy: r.Policy, - Rule: r.Rule, - Source: r.Source, - Scored: r.Scored, - Message: r.Message, - Result: string(r.Result), - Severity: string(r.Severity), - Category: r.Category, - Properties: r.Properties, - Created: r.Timestamp.Seconds, + ResourceID: resource.GetID(), + Resource: resource, + Policy: r.Policy, + Rule: r.Rule, + Source: r.Source, + Scored: r.Scored, + Message: r.Message, + Result: string(r.Result), + Severity: string(r.Severity), + Category: r.Category, + Properties: r.Properties, + Created: r.Timestamp.Seconds, }) } @@ -177,25 +212,93 @@ func MapPolicyReportFilter(polr v1alpha2.ReportInterface) []*PolicyReportFilter return list } -func MapListResult(results []*PolicyReportResult) []*api.ListResult { - list := make([]*api.ListResult, 0, len(results)) - for _, res := range results { - list = append(list, &api.ListResult{ - ID: res.ID, - Namespace: res.Resource.Namespace, - Kind: res.Resource.Kind, - APIVersion: res.Resource.APIVersion, - Name: res.Resource.Name, - Message: res.Message, - Category: res.Category, - Policy: res.Policy, - Rule: res.Rule, - Status: res.Result, - Severity: res.Severity, - Timestamp: res.Created, - Properties: res.Properties, - }) +func MapPolicyReportResource(polr v1alpha2.ReportInterface) []*ResourceResult { + mapping := make(map[string]*ResourceResult) + for _, res := range polr.GetResults() { + resource := polr.GetScope() + if res.HasResource() { + resource = res.GetResource() + } + + if resource == nil { + continue + } + + r := Resource{ + APIVersion: resource.APIVersion, + Kind: resource.Kind, + UID: string(resource.UID), + Namespace: resource.Namespace, + Name: resource.Name, + } + + id := r.GetID() + res.Category + polr.GetID() + + value, ok := mapping[id] + if !ok { + value = &ResourceResult{ + ID: r.GetID(), + PolicyReportID: polr.GetID(), + Resource: r, + Source: res.Source, + Category: res.Category, + } + + mapping[id] = value + } + + switch res.Result { + case v1alpha2.StatusPass: + value.Pass = value.Pass + 1 + case v1alpha2.StatusSkip: + value.Skip = value.Skip + 1 + case v1alpha2.StatusWarn: + value.Warn = value.Warn + 1 + case v1alpha2.StatusFail: + value.Fail = value.Fail + 1 + case v1alpha2.StatusError: + value.Error = value.Error + 1 + } + + switch res.Severity { + case v1alpha2.SeverityInfo: + value.Info = value.Info + 1 + case v1alpha2.SeverityLow: + value.Low = value.Low + 1 + case v1alpha2.SeverityMedium: + value.Medium = value.Medium + 1 + case v1alpha2.SeverityHigh: + value.High = value.High + 1 + case v1alpha2.SeverityCritical: + value.Critical = value.Critical + 1 + default: + value.Unknown = value.Unknown + 1 + } } - return list + return helper.ToList(mapping) +} + +type Filter struct { + Kinds []string + Categories []string + Namespaces []string + Sources []string + Policies []string + Rules []string + Severities []string + Status []string + Resources []string + ResourceID string + ReportLabel map[string]string + Exclude map[string][]string + Namespaced bool + Search string +} + +type Pagination struct { + Page int + Offset int + SortBy []string + Direction string } diff --git a/pkg/database/views.go b/pkg/database/views.go new file mode 100644 index 00000000..d05dc535 --- /dev/null +++ b/pkg/database/views.go @@ -0,0 +1,71 @@ +package database + +import "github.com/uptrace/bun" + +type Category struct { + bun.BaseModel `bun:"table:policy_report_filter,alias:f"` + + Source string + Name string `bun:"category"` + Result string + Severity string + Count int +} + +type ResourceCategory struct { + bun.BaseModel `bun:"table:policy_report_resource,alias:res"` + + Source string + Name string `bun:"category"` + Pass int + Warn int + Fail int + Error int + Skip int +} + +type ResourceStatusCount struct { + bun.BaseModel `bun:"table:policy_report_resource,alias:res"` + Source string + Pass int + Warn int + Fail int + Error int + Skip int +} + +type ResourceSeverityCount struct { + bun.BaseModel `bun:"table:policy_report_resource,alias:res"` + Source string + Info int + Low int + Medium int + High int + Critical int + Unknown int +} + +type StatusCount struct { + bun.BaseModel `bun:"table:policy_report_filter,alias:f"` + + Source string + Namespace string `bun:"resource_namespace"` + Status string + Count int +} + +type SeverityCount struct { + bun.BaseModel `bun:"table:policy_report_filter,alias:f"` + + Source string + Namespace string `bun:"resource_namespace"` + Severity string + Count int +} + +type ResultProperty struct { + bun.BaseModel `bun:"table:policy_report_result,alias:pr"` + + Namespace string `bun:"resource_namespace"` + Property string `bun:"property"` +} diff --git a/pkg/email/filter.go b/pkg/email/filter.go index 9f679fe3..bbfd9e40 100644 --- a/pkg/email/filter.go +++ b/pkg/email/filter.go @@ -1,22 +1,41 @@ package email import ( + "context" + + "go.uber.org/zap" + + "github.com/kyverno/policy-reporter/pkg/kubernetes/namespaces" "github.com/kyverno/policy-reporter/pkg/validate" ) type Filter struct { + client namespaces.Client namespace validate.RuleSets sources validate.RuleSets } func (f Filter) ValidateSource(source string) bool { - return validate.ContainsRuleSet(source, f.sources) + return validate.MatchRuleSet(source, f.sources) } func (f Filter) ValidateNamespace(namespace string) bool { - return validate.Namespace(namespace, f.namespace) + ruleset := f.namespace + + if len(f.namespace.Selector) > 0 { + list, err := f.client.List(context.Background(), f.namespace.Selector) + if err != nil { + zap.L().Error("failed to resolve namespace selector", zap.Error(err)) + } + + ruleset = validate.RuleSets{ + Include: list, + } + } + + return validate.Namespace(namespace, ruleset) } -func NewFilter(namespaces, sources validate.RuleSets) Filter { - return Filter{namespaces, sources} +func NewFilter(client namespaces.Client, namespaces, sources validate.RuleSets) Filter { + return Filter{client, namespaces, sources} } diff --git a/pkg/email/filter_test.go b/pkg/email/filter_test.go index 356f90d7..f1fc03a3 100644 --- a/pkg/email/filter_test.go +++ b/pkg/email/filter_test.go @@ -9,7 +9,7 @@ import ( func Test_Filters(t *testing.T) { t.Run("Validate Default", func(t *testing.T) { - filter := email.NewFilter(validate.RuleSets{}, validate.RuleSets{}) + filter := email.NewFilter(nil, validate.RuleSets{}, validate.RuleSets{}) if !filter.ValidateNamespace("test") { t.Errorf("Unexpected Validation Result without configured rules") diff --git a/pkg/email/summary/fixtures_test.go b/pkg/email/summary/fixtures_test.go index dbd17099..462854c0 100644 --- a/pkg/email/summary/fixtures_test.go +++ b/pkg/email/summary/fixtures_test.go @@ -10,7 +10,7 @@ import ( ) var ( - filter = email.NewFilter(validate.RuleSets{}, validate.RuleSets{}) + filter = email.NewFilter(nil, validate.RuleSets{}, validate.RuleSets{}) logger = zap.NewNop() ) diff --git a/pkg/email/summary/generator_test.go b/pkg/email/summary/generator_test.go index e473b42f..ce447412 100644 --- a/pkg/email/summary/generator_test.go +++ b/pkg/email/summary/generator_test.go @@ -81,7 +81,7 @@ func Test_GenerateDataWithSourceFilter(t *testing.T) { _, _ = cClient.Create(ctx, fixtures.EmptyClusterPolicyReport, v1.CreateOptions{}) _, _ = cClient.Create(ctx, fixtures.KyvernoClusterPolicyReport, v1.CreateOptions{}) - generator := summary.NewGenerator(client, email.NewFilter(validate.RuleSets{}, validate.RuleSets{Include: []string{"test"}}), true) + generator := summary.NewGenerator(client, email.NewFilter(nil, validate.RuleSets{}, validate.RuleSets{Include: []string{"test"}}), true) data, err := generator.GenerateData(ctx) if err != nil { @@ -113,7 +113,7 @@ func Test_FilterSourcesBySource(t *testing.T) { t.Fatalf("unexpected error: %s", err) } - data = summary.FilterSources(data, email.NewFilter(validate.RuleSets{}, validate.RuleSets{Include: []string{"Kyverno"}}), true) + data = summary.FilterSources(data, email.NewFilter(nil, validate.RuleSets{}, validate.RuleSets{Include: []string{"Kyverno"}}), true) if len(data) != 1 { t.Fatalf("expected one source left, got: %d", len(data)) } @@ -139,7 +139,7 @@ func Test_FilterSourcesByNamespace(t *testing.T) { t.Fatalf("unexpected error: %s", err) } - data = summary.FilterSources(data, email.NewFilter(validate.RuleSets{Exclude: []string{"kyverno"}}, validate.RuleSets{}), true) + data = summary.FilterSources(data, email.NewFilter(nil, validate.RuleSets{Exclude: []string{"kyverno"}}, validate.RuleSets{}), true) source := data[0] if source.Name != "Kyverno" { source = data[1] @@ -170,7 +170,7 @@ func Test_RemoveEmptySource(t *testing.T) { t.Fatalf("unexpected error: %s", err) } - data = summary.FilterSources(data, email.NewFilter(validate.RuleSets{Exclude: []string{"kyverno"}}, validate.RuleSets{}), false) + data = summary.FilterSources(data, email.NewFilter(nil, validate.RuleSets{Exclude: []string{"kyverno"}}, validate.RuleSets{}), false) if len(data) != 1 { t.Fatalf("expected one source left, got: %d", len(data)) } diff --git a/pkg/email/violations/fixtures_test.go b/pkg/email/violations/fixtures_test.go index 77bc2c11..dc38ce7e 100644 --- a/pkg/email/violations/fixtures_test.go +++ b/pkg/email/violations/fixtures_test.go @@ -10,7 +10,7 @@ import ( ) var ( - filter = email.NewFilter(validate.RuleSets{}, validate.RuleSets{}) + filter = email.NewFilter(nil, validate.RuleSets{}, validate.RuleSets{}) logger = zap.NewNop() ) diff --git a/pkg/email/violations/generator_test.go b/pkg/email/violations/generator_test.go index 519b92a7..f9adbc88 100644 --- a/pkg/email/violations/generator_test.go +++ b/pkg/email/violations/generator_test.go @@ -105,7 +105,7 @@ func Test_GenerateDataWithSourceFilter(t *testing.T) { _, _ = cClient.Create(ctx, fixtures.EmptyClusterPolicyReport, v1.CreateOptions{}) _, _ = cClient.Create(ctx, fixtures.KyvernoClusterPolicyReport, v1.CreateOptions{}) - generator := violations.NewGenerator(client, email.NewFilter(validate.RuleSets{}, validate.RuleSets{Include: []string{"test"}}), true) + generator := violations.NewGenerator(client, email.NewFilter(nil, validate.RuleSets{}, validate.RuleSets{Include: []string{"test"}}), true) data, err := generator.GenerateData(ctx) if err != nil { @@ -137,7 +137,7 @@ func Test_FilterSourcesBySource(t *testing.T) { t.Fatalf("unexpected error: %s", err) } - data = violations.FilterSources(data, email.NewFilter(validate.RuleSets{}, validate.RuleSets{Include: []string{"Kyverno"}}), true) + data = violations.FilterSources(data, email.NewFilter(nil, validate.RuleSets{}, validate.RuleSets{Include: []string{"Kyverno"}}), true) if len(data) != 1 { t.Fatalf("expected one source left, got: %d", len(data)) } @@ -163,7 +163,7 @@ func Test_FilterSourcesByNamespace(t *testing.T) { t.Fatalf("unexpected error: %s", err) } - data = violations.FilterSources(data, email.NewFilter(validate.RuleSets{Exclude: []string{"kyverno"}}, validate.RuleSets{}), true) + data = violations.FilterSources(data, email.NewFilter(nil, validate.RuleSets{Exclude: []string{"kyverno"}}, validate.RuleSets{}), true) source := data[0] if source.Name != "Kyverno" { source = data[1] @@ -194,7 +194,7 @@ func Test_RemoveEmptySource(t *testing.T) { t.Fatalf("unexpected error: %s", err) } - data = violations.FilterSources(data, email.NewFilter(validate.RuleSets{Exclude: []string{"kyverno"}}, validate.RuleSets{}), false) + data = violations.FilterSources(data, email.NewFilter(nil, validate.RuleSets{Exclude: []string{"kyverno"}}, validate.RuleSets{}), false) if len(data) != 1 { t.Fatalf("expected one source left, got: %d", len(data)) } diff --git a/pkg/fixtures/policy_reports.go b/pkg/fixtures/policy_reports.go index 398747bc..cde8a261 100644 --- a/pkg/fixtures/policy_reports.go +++ b/pkg/fixtures/policy_reports.go @@ -60,6 +60,7 @@ var DefaultPolicyReport = &v1alpha2.PolicyReport{ Scored: true, Policy: "priority-test", Timestamp: v1.Timestamp{Seconds: 1614093000}, + Source: "test", }, { ID: "12347", @@ -86,6 +87,42 @@ var DefaultPolicyReport = &v1alpha2.PolicyReport{ }, } +var ScopePolicyReport = &v1alpha2.PolicyReport{ + ObjectMeta: v1.ObjectMeta{ + Name: "policy-report", + Namespace: "test", + }, + Summary: v1alpha2.PolicyReportSummary{ + Pass: 0, + Skip: 0, + Warn: 0, + Fail: 3, + Error: 0, + }, + Scope: &corev1.ObjectReference{ + APIVersion: "v1", + Kind: "Deployment", + Name: "nginx", + Namespace: "test", + UID: "dfd57c50-f30c-4729-b63f-b1954d8988d1", + }, + Results: []v1alpha2.PolicyReportResult{ + { + ID: "12348", + Message: "message", + Result: v1alpha2.StatusFail, + Scored: true, + Policy: "required-label", + Rule: "app-label-required", + Timestamp: v1.Timestamp{Seconds: 1614093000}, + Source: "test", + Category: "test", + Severity: v1alpha2.SeverityHigh, + Properties: map[string]string{"version": "1.2.0"}, + }, + }, +} + var MultiResourcePolicyReport = &v1alpha2.PolicyReport{ ObjectMeta: v1.ObjectMeta{ Name: "policy-report", diff --git a/pkg/fixtures/policy_results.go b/pkg/fixtures/policy_results.go index 83e5c231..b0b1f68d 100644 --- a/pkg/fixtures/policy_results.go +++ b/pkg/fixtures/policy_results.go @@ -11,7 +11,6 @@ var PassResult = v1alpha2.PolicyReportResult{ Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/", Policy: "require-requests-and-limits-required", Rule: "autogen-check-for-requests-and-limits", - Priority: v1alpha2.WarningPriority, Result: v1alpha2.StatusPass, Severity: v1alpha2.SeverityHigh, Category: "resources", @@ -32,7 +31,6 @@ var PassPodResult = v1alpha2.PolicyReportResult{ Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/", Policy: "require-requests-and-limits-required", Rule: "autogen-check-for-requests-and-limits", - Priority: v1alpha2.WarningPriority, Result: v1alpha2.StatusPass, Category: "Best Practices", Scored: true, @@ -52,7 +50,6 @@ var TrivyResult = v1alpha2.PolicyReportResult{ Message: "validation error", Policy: "policy", Rule: "rule", - Priority: v1alpha2.WarningPriority, Result: v1alpha2.StatusFail, Category: "Best Practices", Scored: true, @@ -64,7 +61,6 @@ var FailResult = v1alpha2.PolicyReportResult{ Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/", Policy: "require-requests-and-limits-required", Rule: "autogen-check-for-requests-and-limits", - Priority: v1alpha2.WarningPriority, Result: v1alpha2.StatusFail, Severity: v1alpha2.SeverityHigh, Category: "resources", @@ -84,7 +80,6 @@ var FailDisallowRuleResult = v1alpha2.PolicyReportResult{ Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/", Policy: "disallow-policy", Rule: "disallow-policy", - Priority: v1alpha2.WarningPriority, Result: v1alpha2.StatusFail, Severity: v1alpha2.SeverityHigh, Category: "resources", @@ -104,7 +99,6 @@ var FailPodResult = v1alpha2.PolicyReportResult{ Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/", Policy: "require-requests-and-limits-required", Rule: "autogen-check-for-requests-and-limits", - Priority: v1alpha2.WarningPriority, Result: v1alpha2.StatusFail, Category: "Best Practices", Scored: true, @@ -122,7 +116,6 @@ var FailResultWithoutResource = v1alpha2.PolicyReportResult{ Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/", Policy: "require-requests-and-limits-required", Rule: "autogen-check-for-requests-and-limits", - Priority: v1alpha2.WarningPriority, Result: v1alpha2.StatusFail, Severity: v1alpha2.SeverityHigh, Category: "resources", @@ -135,7 +128,6 @@ var PassNamespaceResult = v1alpha2.PolicyReportResult{ Message: "validation error: The label `test` is required. Rule check-for-GetLabels()-on-namespace", Policy: "require-ns-GetLabels()", Rule: "check-for-GetLabels()-on-namespace", - Priority: v1alpha2.ErrorPriority, Result: v1alpha2.StatusPass, Category: "namespaces", Severity: v1alpha2.SeverityMedium, @@ -154,7 +146,6 @@ var FailNamespaceResult = v1alpha2.PolicyReportResult{ Message: "validation error: The label `test` is required. Rule check-for-GetLabels()-on-namespace", Policy: "require-ns-GetLabels()", Rule: "check-for-GetLabels()-on-namespace", - Priority: v1alpha2.WarningPriority, Result: v1alpha2.StatusFail, Category: "namespaces", Severity: v1alpha2.SeverityHigh, @@ -172,7 +163,6 @@ var ScopeResult = v1alpha2.PolicyReportResult{ Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/", Policy: "require-requests-and-limits-required", Rule: "autogen-check-for-requests-and-limits", - Priority: v1alpha2.WarningPriority, Result: v1alpha2.StatusFail, Severity: v1alpha2.SeverityHigh, Category: "resources", diff --git a/pkg/fixtures/target_results.go b/pkg/fixtures/target_results.go index 06fbaefc..65399c06 100644 --- a/pkg/fixtures/target_results.go +++ b/pkg/fixtures/target_results.go @@ -16,7 +16,6 @@ var CompleteTargetSendResult = v1alpha2.PolicyReportResult{ Policy: "require-requests-and-limits-required", Rule: "autogen-check-for-requests-and-limits", Timestamp: v1.Timestamp{Seconds: seconds}, - Priority: v1alpha2.WarningPriority, Result: v1alpha2.StatusFail, Severity: v1alpha2.SeverityHigh, Category: "resources", @@ -33,11 +32,10 @@ var CompleteTargetSendResult = v1alpha2.PolicyReportResult{ } var MinimalTargetSendResult = v1alpha2.PolicyReportResult{ - Message: "validation error: label required. Rule app-label-required failed at path /spec/template/spec/containers/0/resources/requests/", - Policy: "app-label-requirement", - Priority: v1alpha2.CriticalPriority, - Result: v1alpha2.StatusFail, - Scored: true, + Message: "validation error: label required. Rule app-label-required failed at path /spec/template/spec/containers/0/resources/requests/", + Policy: "app-label-requirement", + Result: v1alpha2.StatusFail, + Scored: true, } var EnforceTargetSendResult = v1alpha2.PolicyReportResult{ @@ -45,7 +43,6 @@ var EnforceTargetSendResult = v1alpha2.PolicyReportResult{ Policy: "require-requests-and-limits-required", Rule: "check-for-requests-and-limits", Timestamp: v1.Timestamp{Seconds: seconds}, - Priority: v1alpha2.WarningPriority, Result: v1alpha2.StatusFail, Severity: v1alpha2.SeverityHigh, Category: "resources", @@ -66,7 +63,6 @@ var MissingUIDSendResult = v1alpha2.PolicyReportResult{ Policy: "require-requests-and-limits-required", Rule: "check-for-requests-and-limits", Timestamp: v1.Timestamp{Seconds: seconds}, - Priority: v1alpha2.WarningPriority, Result: v1alpha2.StatusFail, Severity: v1alpha2.SeverityHigh, Category: "resources", @@ -87,7 +83,6 @@ var MissingAPIVersionSendResult = v1alpha2.PolicyReportResult{ Policy: "require-requests-and-limits-required", Rule: "check-for-requests-and-limits", Timestamp: v1.Timestamp{Seconds: seconds}, - Priority: v1alpha2.WarningPriority, Result: v1alpha2.StatusFail, Severity: v1alpha2.SeverityHigh, Category: "resources", @@ -104,33 +99,29 @@ var MissingAPIVersionSendResult = v1alpha2.PolicyReportResult{ } var ErrorSendResult = v1alpha2.PolicyReportResult{ - Message: "validation error: label required. Rule app-label-required failed at path /spec/template/spec/containers/0/resources/requests/", - Policy: "app-label-requirement", - Priority: v1alpha2.ErrorPriority, - Result: v1alpha2.StatusFail, - Scored: true, + Message: "validation error: label required. Rule app-label-required failed at path /spec/template/spec/containers/0/resources/requests/", + Policy: "app-label-requirement", + Result: v1alpha2.StatusFail, + Scored: true, } var CritcalSendResult = v1alpha2.PolicyReportResult{ - Message: "validation error: label required. Rule app-label-required failed at path /spec/template/spec/containers/0/resources/requests/", - Policy: "app-label-requirement", - Priority: v1alpha2.CriticalPriority, - Result: v1alpha2.StatusFail, - Scored: true, + Message: "validation error: label required. Rule app-label-required failed at path /spec/template/spec/containers/0/resources/requests/", + Policy: "app-label-requirement", + Result: v1alpha2.StatusFail, + Scored: true, } var InfoSendResult = v1alpha2.PolicyReportResult{ - Message: "validation error: label required. Rule app-label-required failed at path /spec/template/spec/containers/0/resources/requests/", - Policy: "app-label-requirement", - Priority: v1alpha2.InfoPriority, - Result: v1alpha2.StatusFail, - Scored: true, + Message: "validation error: label required. Rule app-label-required failed at path /spec/template/spec/containers/0/resources/requests/", + Policy: "app-label-requirement", + Result: v1alpha2.StatusFail, + Scored: true, } var DebugSendResult = v1alpha2.PolicyReportResult{ - Message: "validation error: label required. Rule app-label-required failed at path /spec/template/spec/containers/0/resources/requests/", - Policy: "app-label-requirement", - Priority: v1alpha2.DebugPriority, - Result: v1alpha2.StatusFail, - Scored: true, + Message: "validation error: label required. Rule app-label-required failed at path /spec/template/spec/containers/0/resources/requests/", + Policy: "app-label-requirement", + Result: v1alpha2.StatusFail, + Scored: true, } diff --git a/pkg/helper/chunk_slice.go b/pkg/helper/chunk_slice.go new file mode 100644 index 00000000..61d9f36a --- /dev/null +++ b/pkg/helper/chunk_slice.go @@ -0,0 +1,16 @@ +package helper + +func ChunkSlice[K interface{}](slice []K, chunkSize int) [][]K { + var chunks [][]K + for i := 0; i < len(slice); i += chunkSize { + end := i + chunkSize + + if end > len(slice) { + end = len(slice) + } + + chunks = append(chunks, slice[i:end]) + } + + return chunks +} diff --git a/pkg/helper/http_test.go b/pkg/helper/http_test.go new file mode 100644 index 00000000..169e3053 --- /dev/null +++ b/pkg/helper/http_test.go @@ -0,0 +1,43 @@ +package helper_test + +import ( + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/kyverno/policy-reporter/pkg/helper" +) + +func TestSendJSONResponse(t *testing.T) { + t.Run("success response", func(t *testing.T) { + w := httptest.NewRecorder() + + helper.SendJSONResponse(w, []string{"default", "user"}, nil) + + assert.Equal(t, http.StatusOK, w.Code) + + resp := make([]string, 0, 2) + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, []string{"default", "user"}, resp) + }) + + t.Run("error response", func(t *testing.T) { + w := httptest.NewRecorder() + + helper.SendJSONResponse(w, nil, errors.New("error")) + + assert.Equal(t, http.StatusInternalServerError, w.Code) + + resp := make(map[string]string, 0) + + json.NewDecoder(w.Body).Decode(&resp) + + assert.Equal(t, map[string]string{"message": "error"}, resp) + }) +} diff --git a/pkg/helper/title.go b/pkg/helper/title.go new file mode 100644 index 00000000..471876f5 --- /dev/null +++ b/pkg/helper/title.go @@ -0,0 +1,12 @@ +package helper + +import ( + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +var caser = cases.Title(language.English, cases.NoLower) + +func Title(s string) string { + return caser.String(s) +} diff --git a/pkg/helper/utils.go b/pkg/helper/utils.go index d8ade1f5..a4af84ce 100644 --- a/pkg/helper/utils.go +++ b/pkg/helper/utils.go @@ -1,6 +1,8 @@ package helper -import "strings" +import ( + "strings" +) func Contains(source string, sources []string) bool { for _, s := range sources { @@ -12,6 +14,44 @@ func Contains(source string, sources []string) bool { return false } +func ToList[T any, R comparable](mapping map[R]T) []T { + list := make([]T, 0, len(mapping)) + for _, i := range mapping { + list = append(list, i) + } + + return list +} + +func Map[T any, R any](source []T, cb func(T) R) []R { + list := make([]R, 0, len(source)) + for _, i := range source { + list = append(list, cb(i)) + } + + return list +} + +func MapSlice[T any, R any, Z comparable](source map[Z]T, cb func(T) R) []R { + list := make([]R, 0, len(source)) + for _, i := range source { + list = append(list, cb(i)) + } + + return list +} + +func ConvertMap(m map[string]any) map[string]string { + n := make(map[string]string, len(m)) + for k, v := range m { + if l, ok := v.(string); ok { + n[k] = l + } + } + + return n +} + func Defaults(s, f string) string { if s != "" { return s @@ -19,3 +59,26 @@ func Defaults(s, f string) string { return f } + +func ToPointer[T any](s T) *T { + return &s +} + +func Filter[T any](s []T, keep func(T) bool) []T { + d := make([]T, 0, len(s)) + for _, n := range s { + if keep(n) { + d = append(d, n) + } + } + return d +} + +func Find[T any](s []T, keep func(T) bool, fallback T) T { + for _, n := range s { + if keep(n) { + return n + } + } + return fallback +} diff --git a/pkg/helper/utils_test.go b/pkg/helper/utils_test.go new file mode 100644 index 00000000..d11aa639 --- /dev/null +++ b/pkg/helper/utils_test.go @@ -0,0 +1,75 @@ +package helper_test + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/kyverno/policy-reporter/pkg/helper" +) + +func TestContains(t *testing.T) { + assert.True(t, helper.Contains("kyverno", []string{"test", "kyverno", "trivy"})) + assert.False(t, helper.Contains("kube-bench", []string{"test", "kyverno", "trivy"})) +} + +func TestToList(t *testing.T) { + result := helper.ToList(map[string]string{ + "first": "kyverno", + "second": "trivy", + }) + + assert.Equal(t, 2, len(result)) + assert.Contains(t, result, "kyverno") + assert.Contains(t, result, "trivy") +} + +func TestMap(t *testing.T) { + assert.Equal(t, []string{"kyverno", "trivy"}, helper.Map([]string{"source_kyverno", "source_trivy"}, func(value string) string { + return strings.TrimPrefix(value, "source_") + })) +} + +func TestConvertMap(t *testing.T) { + assert.Equal(t, map[string]string{"first": "kyverno", "second": "trivy"}, helper.ConvertMap(map[string]any{ + "first": "kyverno", + "second": "trivy", + "third": 3, + })) +} + +func TestDetauls(t *testing.T) { + assert.Equal(t, "fallback", helper.Defaults("", "fallback")) + assert.Equal(t, "value", helper.Defaults("value", "fallback")) +} + +func TestToPointer(t *testing.T) { + value := "test" + number := 5 + + assert.Equal(t, &value, helper.ToPointer(value)) + assert.Equal(t, &number, helper.ToPointer(number)) +} + +func TestFind(t *testing.T) { + list := []string{"test", "find", "item"} + + assert.Equal(t, "find", helper.Find(list, func(t string) bool { return t == "find" }, "")) + assert.Equal(t, "fallback", helper.Find(list, func(t string) bool { return t == "invalid" }, "fallback")) +} + +func TestMapSlice(t *testing.T) { + mapped := helper.MapSlice(map[int]string{2: "source_kyverno", 3: "source_trivy"}, func(value string) string { + return strings.TrimPrefix(value, "source_") + }) + + assert.Contains(t, mapped, "kyverno") + assert.Contains(t, mapped, "trivy") +} + +func TestFilter(t *testing.T) { + list := []string{"test", "find", "item", "", ""} + + assert.Equal(t, []string{"test", "find", "item"}, helper.Filter(list, func(t string) bool { return t != "" })) +} diff --git a/pkg/kubernetes/jobs/client.go b/pkg/kubernetes/jobs/client.go new file mode 100644 index 00000000..1de8709a --- /dev/null +++ b/pkg/kubernetes/jobs/client.go @@ -0,0 +1,32 @@ +package jobs + +import ( + "context" + + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/client-go/kubernetes/typed/batch/v1" + + "github.com/kyverno/policy-reporter/pkg/kubernetes" +) + +type Client interface { + Get(scope *corev1.ObjectReference) (*batchv1.Job, error) +} + +type k8sClient struct { + client v1.BatchV1Interface +} + +func (c *k8sClient) Get(scope *corev1.ObjectReference) (*batchv1.Job, error) { + return kubernetes.Retry(func() (*batchv1.Job, error) { + return c.client.Jobs(scope.Namespace).Get(context.Background(), scope.Name, metav1.GetOptions{}) + }) +} + +func NewClient(client v1.BatchV1Interface) Client { + return &k8sClient{ + client: client, + } +} diff --git a/pkg/kubernetes/namespaces/client.go b/pkg/kubernetes/namespaces/client.go new file mode 100644 index 00000000..c07b9dca --- /dev/null +++ b/pkg/kubernetes/namespaces/client.go @@ -0,0 +1,54 @@ +package namespaces + +import ( + "context" + + gocache "github.com/patrickmn/go-cache" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/client-go/kubernetes/typed/core/v1" + + "github.com/kyverno/policy-reporter/pkg/helper" + "github.com/kyverno/policy-reporter/pkg/kubernetes" +) + +type Client interface { + List(context.Context, map[string]string) ([]string, error) +} + +type k8sClient struct { + client v1.NamespaceInterface + cache *gocache.Cache +} + +func (c *k8sClient) List(ctx context.Context, selector map[string]string) ([]string, error) { + labelSelector := metav1.FormatLabelSelector(&metav1.LabelSelector{MatchLabels: selector}) + if cached, ok := c.cache.Get(labelSelector); ok { + return cached.([]string), nil + } + + list, err := kubernetes.Retry(func() ([]string, error) { + namespaces, err := c.client.List(ctx, metav1.ListOptions{LabelSelector: labelSelector}) + if err != nil { + return nil, err + } + + return helper.Map(namespaces.Items, func(ns corev1.Namespace) string { + return ns.Name + }), nil + }) + if err != nil { + return nil, err + } + + c.cache.Set(labelSelector, list, 0) + + return list, nil +} + +func NewClient(secretClient v1.NamespaceInterface, cache *gocache.Cache) Client { + return &k8sClient{ + client: secretClient, + cache: cache, + } +} diff --git a/pkg/kubernetes/namespaces/client_test.go b/pkg/kubernetes/namespaces/client_test.go new file mode 100644 index 00000000..310c2db7 --- /dev/null +++ b/pkg/kubernetes/namespaces/client_test.go @@ -0,0 +1,100 @@ +package namespaces_test + +import ( + "context" + "errors" + "testing" + + gocache "github.com/patrickmn/go-cache" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" + v1 "k8s.io/client-go/kubernetes/typed/core/v1" + + "github.com/kyverno/policy-reporter/pkg/kubernetes/namespaces" +) + +func newFakeClient() v1.NamespaceInterface { + return fake.NewSimpleClientset( + &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + Labels: map[string]string{ + "team": "team-a", + "name": "default", + }, + }, + }, + &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user", + Labels: map[string]string{ + "team": "team-a", + "name": "user", + }, + }, + }, + ).CoreV1().Namespaces() +} + +type nsErrorClient struct { + v1.NamespaceInterface +} + +func (s *nsErrorClient) List(ctx context.Context, opts metav1.ListOptions) (*corev1.NamespaceList, error) { + return nil, errors.New("error") +} + +func TestClient(t *testing.T) { + t.Run("read from api", func(t *testing.T) { + client := namespaces.NewClient(newFakeClient(), gocache.New(gocache.DefaultExpiration, gocache.DefaultExpiration)) + + list, err := client.List(context.Background(), map[string]string{"name": "default"}) + + assert.Nil(t, err) + assert.Equal(t, 1, len(list)) + }) + + t.Run("read from cache", func(t *testing.T) { + fake := newFakeClient() + cache := gocache.New(gocache.NoExpiration, gocache.NoExpiration) + + client := namespaces.NewClient(fake, cache) + + list, err := client.List(context.Background(), map[string]string{"team": "team-a"}) + + assert.Nil(t, err) + assert.Equal(t, 2, len(list)) + + fake.Create(context.Background(), &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "finance", + Labels: map[string]string{ + "team": "team-a", + "name": "finance", + }, + }, + }, metav1.CreateOptions{}) + + list, err = client.List(context.Background(), map[string]string{"team": "team-a"}) + + assert.Nil(t, err) + assert.Equal(t, 2, len(list)) + + cache.Flush() + + list, err = client.List(context.Background(), map[string]string{"team": "team-a"}) + + assert.Nil(t, err) + assert.Equal(t, 3, len(list)) + }) + t.Run("return error", func(t *testing.T) { + client := namespaces.NewClient(&nsErrorClient{NamespaceInterface: newFakeClient()}, gocache.New(gocache.DefaultExpiration, gocache.DefaultExpiration)) + + _, err := client.List(context.Background(), map[string]string{"team": "team-a"}) + + assert.NotNil(t, err) + assert.Equal(t, "error", err.Error()) + }) +} diff --git a/pkg/kubernetes/pods/client.go b/pkg/kubernetes/pods/client.go new file mode 100644 index 00000000..36bc101a --- /dev/null +++ b/pkg/kubernetes/pods/client.go @@ -0,0 +1,31 @@ +package pods + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/client-go/kubernetes/typed/core/v1" + + "github.com/kyverno/policy-reporter/pkg/kubernetes" +) + +type Client interface { + Get(scope *corev1.ObjectReference) (*corev1.Pod, error) +} + +type k8sClient struct { + client v1.CoreV1Interface +} + +func (c *k8sClient) Get(scope *corev1.ObjectReference) (*corev1.Pod, error) { + return kubernetes.Retry(func() (*corev1.Pod, error) { + return c.client.Pods(scope.Namespace).Get(context.Background(), scope.Name, metav1.GetOptions{}) + }) +} + +func NewClient(client v1.CoreV1Interface) Client { + return &k8sClient{ + client: client, + } +} diff --git a/pkg/kubernetes/policy_report_client.go b/pkg/kubernetes/policy_report_client.go index 44435ec2..e4a65f03 100644 --- a/pkg/kubernetes/policy_report_client.go +++ b/pkg/kubernetes/policy_report_client.go @@ -25,7 +25,7 @@ type k8sPolicyReportClient struct { metaClient metadata.Interface synced bool mx *sync.Mutex - reportFilter *report.Filter + reportFilter *report.MetaFilter stopChan chan struct{} } @@ -110,7 +110,7 @@ func (k *k8sPolicyReportClient) configureInformer(informer cache.SharedIndexInfo } // NewPolicyReportClient new Client for Policy Report Kubernetes API -func NewPolicyReportClient(metaClient metadata.Interface, reportFilter *report.Filter, queue *Queue) report.PolicyReportClient { +func NewPolicyReportClient(metaClient metadata.Interface, reportFilter *report.MetaFilter, queue *Queue) report.PolicyReportClient { return &k8sPolicyReportClient{ metaClient: metaClient, mx: &sync.Mutex{}, diff --git a/pkg/kubernetes/policy_report_client_test.go b/pkg/kubernetes/policy_report_client_test.go index 72e3d281..4bde24eb 100644 --- a/pkg/kubernetes/policy_report_client_test.go +++ b/pkg/kubernetes/policy_report_client_test.go @@ -16,7 +16,7 @@ import ( "github.com/kyverno/policy-reporter/pkg/validate" ) -var filter = report.NewFilter(false, validate.RuleSets{}) +var filter = report.NewMetaFilter(false, validate.RuleSets{}) func Test_PolicyReportWatcher(t *testing.T) { ctx := context.Background() @@ -37,8 +37,9 @@ func Test_PolicyReportWatcher(t *testing.T) { queue := kubernetes.NewQueue( kubernetes.NewDebouncer(0, publisher), - workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "test-queue"), + workqueue.NewTypedRateLimitingQueue(workqueue.DefaultTypedControllerRateLimiter[string]()), restClient.Wgpolicyk8sV1alpha2(), + report.NewSourceFilter(nil, nil, []report.SourceValidation{}), result.NewReconditioner(nil), ) @@ -88,8 +89,9 @@ func Test_ClusterPolicyReportWatcher(t *testing.T) { queue := kubernetes.NewQueue( kubernetes.NewDebouncer(0, publisher), - workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "test-queue"), + workqueue.NewTypedRateLimitingQueue(workqueue.DefaultTypedControllerRateLimiter[string]()), restClient.Wgpolicyk8sV1alpha2(), + report.NewSourceFilter(nil, nil, []report.SourceValidation{}), result.NewReconditioner(nil), ) @@ -129,8 +131,9 @@ func Test_HasSynced(t *testing.T) { queue := kubernetes.NewQueue( kubernetes.NewDebouncer(0, report.NewEventPublisher()), - workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "test-queue"), + workqueue.NewTypedRateLimitingQueue(workqueue.DefaultTypedControllerRateLimiter[string]()), restClient.Wgpolicyk8sV1alpha2(), + report.NewSourceFilter(nil, nil, []report.SourceValidation{}), result.NewReconditioner(nil), ) diff --git a/pkg/kubernetes/queue.go b/pkg/kubernetes/queue.go index e1b81aa9..fdd62235 100644 --- a/pkg/kubernetes/queue.go +++ b/pkg/kubernetes/queue.go @@ -21,12 +21,13 @@ import ( ) type Queue struct { - queue workqueue.RateLimitingInterface + queue workqueue.TypedRateLimitingInterface[string] client v1alpha2.Wgpolicyk8sV1alpha2Interface reconditioner *result.Reconditioner debouncer Debouncer lock *sync.Mutex cache sets.Set[string] + filter *report.SourceFilter } func (q *Queue) Add(obj *v1.PartialObjectMetadata) error { @@ -56,11 +57,10 @@ func (q *Queue) runWorker() { } func (q *Queue) processNextItem() bool { - obj, quit := q.queue.Get() + key, quit := q.queue.Get() if quit { return false } - key := obj.(string) defer q.queue.Done(key) namespace, name, err := cache.SplitMetaNamespaceKey(key) @@ -103,6 +103,10 @@ func (q *Queue) processNextItem() bool { return true } + if ok := q.filter.Validate(polr); !ok { + return true + } + event := func() report.Event { q.lock.Lock() defer q.lock.Unlock() @@ -122,7 +126,7 @@ func (q *Queue) processNextItem() bool { return true } -func (q *Queue) handleErr(err error, key interface{}) { +func (q *Queue) handleErr(err error, key string) { if err == nil { q.queue.Forget(key) return @@ -143,8 +147,9 @@ func (q *Queue) handleErr(err error, key interface{}) { func NewQueue( debouncer Debouncer, - queue workqueue.RateLimitingInterface, + queue workqueue.TypedRateLimitingInterface[string], client v1alpha2.Wgpolicyk8sV1alpha2Interface, + filter *report.SourceFilter, reconditioner *result.Reconditioner, ) *Queue { return &Queue{ @@ -153,6 +158,7 @@ func NewQueue( client: client, cache: sets.New[string](), lock: &sync.Mutex{}, + filter: filter, reconditioner: reconditioner, } } diff --git a/pkg/kubernetes/retry.go b/pkg/kubernetes/retry.go new file mode 100644 index 00000000..33488e82 --- /dev/null +++ b/pkg/kubernetes/retry.go @@ -0,0 +1,41 @@ +package kubernetes + +import ( + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/client-go/util/retry" +) + +func Retry[T any](cb func() (T, error)) (T, error) { + var value T + + err := retry.OnError(retry.DefaultRetry, func(err error) bool { + if _, ok := err.(errors.APIStatus); !ok { + return true + } + + if ok := errors.IsTimeout(err); ok { + return true + } + + if ok := errors.IsServerTimeout(err); ok { + return true + } + + if ok := errors.IsServiceUnavailable(err); ok { + return true + } + + return false + }, func() error { + v, err := cb() + if err != nil { + return err + } + + value = v + + return nil + }) + + return value, err +} diff --git a/pkg/kubernetes/retry_test.go b/pkg/kubernetes/retry_test.go new file mode 100644 index 00000000..ce7fb936 --- /dev/null +++ b/pkg/kubernetes/retry_test.go @@ -0,0 +1,152 @@ +package kubernetes_test + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + kerr "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" + v1 "k8s.io/client-go/kubernetes/typed/core/v1" + + "github.com/kyverno/policy-reporter/pkg/kubernetes" +) + +func newFakeClient() v1.NamespaceInterface { + return fake.NewSimpleClientset( + &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + Labels: map[string]string{ + "team": "team-a", + "name": "default", + }, + }, + }, + &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user", + Labels: map[string]string{ + "team": "team-a", + "name": "user", + }, + }, + }, + ).CoreV1().Namespaces() +} + +type ns struct { + maxRetry int + try int + err bool + v1.NamespaceInterface +} + +func (s *ns) List(ctx context.Context, opts metav1.ListOptions) (*corev1.NamespaceList, error) { + if !s.err { + if s.try >= s.maxRetry { + return s.NamespaceInterface.List(ctx, opts) + } + + s.try++ + } + + return nil, errors.New("error") +} + +func TestRetry(t *testing.T) { + t.Run("direct success", func(t *testing.T) { + client := &ns{NamespaceInterface: newFakeClient()} + + list, err := kubernetes.Retry(func() (*corev1.NamespaceList, error) { + return client.List(context.Background(), metav1.ListOptions{}) + }) + + assert.Nil(t, err) + assert.Equal(t, 2, len(list.Items)) + }) + + t.Run("retry success", func(t *testing.T) { + client := &ns{maxRetry: 1, NamespaceInterface: newFakeClient()} + + list, err := kubernetes.Retry(func() (*corev1.NamespaceList, error) { + return client.List(context.Background(), metav1.ListOptions{}) + }) + + assert.Nil(t, err) + assert.Equal(t, 2, len(list.Items)) + }) + + t.Run("retry error", func(t *testing.T) { + client := &ns{NamespaceInterface: newFakeClient(), err: true} + + _, err := kubernetes.Retry(func() (*corev1.NamespaceList, error) { + return client.List(context.Background(), metav1.ListOptions{}) + }) + + assert.NotNil(t, err) + }) + + t.Run("retry timeout", func(t *testing.T) { + try := 0 + + _, err := kubernetes.Retry(func() (any, error) { + try++ + + return nil, &kerr.StatusError{ + ErrStatus: metav1.Status{Reason: metav1.StatusReasonTimeout}, + } + }) + + assert.Equal(t, 5, try) + assert.NotNil(t, err) + }) + + t.Run("retry server timeout", func(t *testing.T) { + try := 0 + + _, err := kubernetes.Retry(func() (any, error) { + try++ + + return nil, &kerr.StatusError{ + ErrStatus: metav1.Status{Reason: metav1.StatusReasonServerTimeout}, + } + }) + + assert.Equal(t, 5, try) + assert.NotNil(t, err) + }) + + t.Run("retry service unavailable", func(t *testing.T) { + try := 0 + + _, err := kubernetes.Retry(func() (any, error) { + try++ + + return nil, &kerr.StatusError{ + ErrStatus: metav1.Status{Reason: metav1.StatusReasonServiceUnavailable}, + } + }) + + assert.Equal(t, 5, try) + assert.NotNil(t, err) + }) + + t.Run("retry ignore other status", func(t *testing.T) { + try := 0 + + _, err := kubernetes.Retry(func() (any, error) { + try++ + + return nil, &kerr.StatusError{ + ErrStatus: metav1.Status{Reason: metav1.StatusReasonForbidden}, + } + }) + + assert.Equal(t, 1, try) + assert.NotNil(t, err) + }) +} diff --git a/pkg/kubernetes/secrets/client.go b/pkg/kubernetes/secrets/client.go index f9a75497..21e0f9ca 100644 --- a/pkg/kubernetes/secrets/client.go +++ b/pkg/kubernetes/secrets/client.go @@ -2,14 +2,13 @@ package secrets import ( "context" - "strconv" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" v1 "k8s.io/client-go/kubernetes/typed/core/v1" - "k8s.io/client-go/util/retry" + + "github.com/kyverno/policy-reporter/pkg/kubernetes" ) type Values struct { @@ -19,9 +18,9 @@ type Values struct { Username string `json:"username,omitempty"` Password string `json:"password,omitempty"` APIKey string `json:"apiKey,omitempty"` - AccessKeyID string `json:"accessKeyID,omitempty"` + AccessKeyID string `json:"accessKeyId,omitempty"` SecretAccessKey string `json:"secretAccessKey,omitempty"` - AccountID string `json:"accountID,omitempty"` + AccountID string `json:"accountId,omitempty"` KmsKeyID string `json:"kmsKeyId,omitempty"` Token string `json:"token,omitempty"` Credentials string `json:"credentials,omitempty"` @@ -39,31 +38,8 @@ type k8sClient struct { } func (c *k8sClient) Get(ctx context.Context, name string) (Values, error) { - var secret *corev1.Secret - - err := retry.OnError(retry.DefaultRetry, func(err error) bool { - if _, ok := err.(errors.APIStatus); !ok { - return true - } - - if ok := errors.IsTimeout(err); ok { - return true - } - - if ok := errors.IsServerTimeout(err); ok { - return true - } - - if ok := errors.IsServiceUnavailable(err); ok { - return true - } - - return false - }, func() error { - var err error - secret, err = c.client.Get(ctx, name, metav1.GetOptions{}) - - return err + secret, err := kubernetes.Retry(func() (*corev1.Secret, error) { + return c.client.Get(ctx, name, metav1.GetOptions{}) }) values := Values{} @@ -103,7 +79,7 @@ func (c *k8sClient) Get(ctx context.Context, name string) (Values, error) { values.DSN = string(dsn) } - if accessKeyID, ok := secret.Data["accessKeyID"]; ok { + if accessKeyID, ok := secret.Data["accessKeyId"]; ok { values.AccessKeyID = string(accessKeyID) } @@ -115,7 +91,7 @@ func (c *k8sClient) Get(ctx context.Context, name string) (Values, error) { values.KmsKeyID = string(kmsKeyID) } - if accountID, ok := secret.Data["accountID"]; ok { + if accountID, ok := secret.Data["accountId"]; ok { values.AccountID = string(accountID) } diff --git a/pkg/kubernetes/secrets/client_test.go b/pkg/kubernetes/secrets/client_test.go index 8d2864c8..2aa61dd8 100644 --- a/pkg/kubernetes/secrets/client_test.go +++ b/pkg/kubernetes/secrets/client_test.go @@ -27,11 +27,11 @@ func newFakeClient() v1.SecretInterface { "password": []byte("password"), "apiKey": []byte("apiKey"), "webhook": []byte("http://localhost:9200/webhook"), - "accessKeyID": []byte("accessKeyID"), + "accessKeyId": []byte("accessKeyId"), "secretAccessKey": []byte("secretAccessKey"), "kmsKeyId": []byte("kmsKeyId"), "token": []byte("token"), - "accountID": []byte("accountID"), + "accountId": []byte("accountId"), "database": []byte("database"), "dsn": []byte("dsn"), "typelessApi": []byte("false"), @@ -68,7 +68,7 @@ func Test_Client(t *testing.T) { t.Errorf("Unexpected ApiKey: %s", values.APIKey) } - if values.AccessKeyID != "accessKeyID" { + if values.AccessKeyID != "accessKeyId" { t.Errorf("Unexpected AccessKeyID: %s", values.AccessKeyID) } @@ -84,8 +84,8 @@ func Test_Client(t *testing.T) { t.Errorf("Unexpected KmsKeyId: %s", values.KmsKeyID) } - if values.AccountID != "accountID" { - t.Errorf("Unexpected AccountID: %s", values.AccountID) + if values.AccountID != "accountId" { + t.Errorf("Unexpected accountId: %s", values.AccountID) } if values.Database != "database" { @@ -97,7 +97,7 @@ func Test_Client(t *testing.T) { } if values.TypelessAPI { - t.Errorf("Unexpected TypelessApi: %t", values.TypelessAPI) + t.Errorf("Unexpected TypelessAPI: %t", values.TypelessAPI) } }) diff --git a/pkg/kubernetes/secrets/informer.go b/pkg/kubernetes/secrets/informer.go new file mode 100644 index 00000000..dacda08e --- /dev/null +++ b/pkg/kubernetes/secrets/informer.go @@ -0,0 +1,122 @@ +package secrets + +import ( + "fmt" + "sync" + "time" + + "go.uber.org/zap" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/metadata" + "k8s.io/client-go/metadata/metadatainformer" + "k8s.io/client-go/tools/cache" + + "github.com/kyverno/policy-reporter/pkg/target" +) + +type Informer interface { + Sync(targets *target.Collection, stopper chan struct{}) error + HasSynced() bool +} + +type informer struct { + metaClient metadata.Interface + synced bool + mx *sync.Mutex + stopChan chan struct{} + factory target.Factory + namespace string +} + +func (k *informer) HasSynced() bool { + return k.synced +} + +func (k *informer) Sync(targets *target.Collection, stopper chan struct{}) error { + k.stopChan = stopper + + factory := metadatainformer.NewFilteredSharedInformerFactory(k.metaClient, 15*time.Minute, k.namespace, nil) + + informer := k.configureInformer(targets, factory.ForResource(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}).Informer()) + + factory.Start(stopper) + + if informer != nil && !cache.WaitForCacheSync(stopper, informer.HasSynced) { + return fmt.Errorf("failed to sync secrets") + } + + k.synced = true + + zap.L().Info("secret informer sync completed") + + return nil +} + +func (k *informer) configureInformer(targets *target.Collection, informer cache.SharedIndexInformer) cache.SharedIndexInformer { + informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + UpdateFunc: func(_, newObj interface{}) { + if s, ok := newObj.(*v1.PartialObjectMetadata); ok { + for _, t := range targets.Targets() { + if t.Secret() == s.Name { + targets.Update(k.UpdateTarget(t, s.Name)) + } + } + } + }, + }) + + informer.SetWatchErrorHandler(func(_ *cache.Reflector, _ error) { + k.synced = false + }) + + return informer +} + +func (k *informer) UpdateTarget(t *target.Target, secret string) *target.Target { + updatedTarget := t + switch t.Type { + case target.Loki: + updatedTarget = createClients(t.Config, t.ParentConfig, k.factory.CreateLokiTarget) + case target.Elasticsearch: + updatedTarget = createClients(t.Config, t.ParentConfig, k.factory.CreateLokiTarget) + case target.Slack: + updatedTarget = createClients(t.Config, t.ParentConfig, k.factory.CreateSlackTarget) + case target.Discord: + updatedTarget = createClients(t.Config, t.ParentConfig, k.factory.CreateDiscordTarget) + case target.Teams: + updatedTarget = createClients(t.Config, t.ParentConfig, k.factory.CreateTeamsTarget) + case target.Webhook: + updatedTarget = createClients(t.Config, t.ParentConfig, k.factory.CreateWebhookTarget) + case target.Telegram: + updatedTarget = createClients(t.Config, t.ParentConfig, k.factory.CreateTelegramTarget) + case target.GoogleChat: + updatedTarget = createClients(t.Config, t.ParentConfig, k.factory.CreateGoogleChatTarget) + case target.S3: + updatedTarget = createClients(t.Config, t.ParentConfig, k.factory.CreateS3Target) + case target.Kinesis: + updatedTarget = createClients(t.Config, t.ParentConfig, k.factory.CreateKinesisTarget) + case target.SecurityHub: + updatedTarget = createClients(t.Config, t.ParentConfig, k.factory.CreateSecurityHubTarget) + case target.GCS: + updatedTarget = createClients(t.Config, t.ParentConfig, k.factory.CreateGCSTarget) + } + + updatedTarget.ID = t.ID + + return updatedTarget +} + +// NewPolicyReportClient new Client for Policy Report Kubernetes API +func NewInformer(metaClient metadata.Interface, factory target.Factory, namespace string) Informer { + return &informer{ + metaClient: metaClient, + mx: new(sync.Mutex), + factory: factory, + namespace: namespace, + } +} + +func createClients[T any](config, parent any, mapper func(*target.Config[T], *target.Config[T]) *target.Target) *target.Target { + return mapper(config.(*target.Config[T]), parent.(*target.Config[T])) +} diff --git a/pkg/kubernetes/secrets/informer_test.go b/pkg/kubernetes/secrets/informer_test.go new file mode 100644 index 00000000..c7fd4bc8 --- /dev/null +++ b/pkg/kubernetes/secrets/informer_test.go @@ -0,0 +1,67 @@ +package secrets_test + +import ( + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + metafake "k8s.io/client-go/metadata/fake" + + "github.com/kyverno/policy-reporter/pkg/kubernetes/secrets" + "github.com/kyverno/policy-reporter/pkg/target" + "github.com/kyverno/policy-reporter/pkg/target/factory" + "github.com/kyverno/policy-reporter/pkg/target/webhook" +) + +func NewFakeMetaClient() (*metafake.FakeMetadataClient, metafake.MetadataClient) { + s := metafake.NewTestScheme() + metav1.AddMetaToScheme(s) + + client := metafake.NewSimpleMetadataClient(s) + return client, client.Resource(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}).Namespace("default").(metafake.MetadataClient) +} + +func Test_SecretInformer(t *testing.T) { + stop := make(chan struct{}) + defer close(stop) + + t.Run("update secretRef", func(t *testing.T) { + collection := target.NewCollection( + &target.Target{ + ID: uuid.NewString(), + Type: target.Webhook, + Client: webhook.NewClient(webhook.Options{ + ClientOptions: target.ClientOptions{ + Name: "Webhook", + }, + }), + Config: &target.Config[target.WebhookOptions]{ + Name: "Webhook", + SecretRef: secretName, + Config: &target.WebhookOptions{}, + }, + ParentConfig: &target.Config[target.WebhookOptions]{Config: &target.WebhookOptions{}}, + }, + ) + + client, secret := NewFakeMetaClient() + + informer := secrets.NewInformer(client, factory.NewFactory(secrets.NewClient(newFakeClient()), target.NewResultFilterFactory(nil)), "default") + + err := informer.Sync(collection, stop) + assert.Nil(t, err) + + assert.True(t, informer.HasSynced()) + + secret.CreateFake(&metav1.PartialObjectMetadata{ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: "default"}}, metav1.CreateOptions{}) + time.Sleep(1 * time.Second) + + secret.UpdateFake(&metav1.PartialObjectMetadata{ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: "default"}}, metav1.UpdateOptions{}) + time.Sleep(1 * time.Second) + + assert.Equal(t, collection.Targets()[0].Config.(*target.Config[target.WebhookOptions]).Config.Webhook, "http://localhost:9200/webhook") + }) +} diff --git a/pkg/leaderelection/client.go b/pkg/leaderelection/client.go index 6444a9cd..76b02d80 100644 --- a/pkg/leaderelection/client.go +++ b/pkg/leaderelection/client.go @@ -45,7 +45,13 @@ func (c *Client) RegisterOnNew(callback func(currentID string, lockID string)) * } func (c *Client) Run(ctx context.Context) error { - k8sleaderelection.RunOrDie(ctx, k8sleaderelection.LeaderElectionConfig{ + k8sleaderelection.RunOrDie(ctx, c.CreateConfig()) + + return errors.New("leaderelection stopped") +} + +func (c *Client) CreateConfig() k8sleaderelection.LeaderElectionConfig { + return k8sleaderelection.LeaderElectionConfig{ Lock: c.CreateLock(), ReleaseOnCancel: c.releaseOnCancel, LeaseDuration: c.leaseDuration, @@ -58,9 +64,7 @@ func (c *Client) Run(ctx context.Context) error { c.onNewLeader(identity, c.identity) }, }, - }) - - return errors.New("leaderelection stopped") + } } func (c *Client) CreateLock() *resourcelock.LeaseLock { diff --git a/pkg/leaderelection/client_test.go b/pkg/leaderelection/client_test.go index 76fd8b84..4228a79e 100644 --- a/pkg/leaderelection/client_test.go +++ b/pkg/leaderelection/client_test.go @@ -5,30 +5,42 @@ import ( "testing" "time" - "github.com/kyverno/policy-reporter/pkg/leaderelection" + "github.com/stretchr/testify/assert" "k8s.io/client-go/kubernetes/typed/coordination/v1/fake" + + "github.com/kyverno/policy-reporter/pkg/leaderelection" ) func TestClient(t *testing.T) { - client := leaderelection.New(&fake.FakeCoordinationV1{}, "policy-reporter", "namespace", "pod-123", time.Second, time.Second, time.Second, false) + client := leaderelection.New(&fake.FakeCoordinationV1{}, "policy-reporter", "namespace", "pod-123", time.Second, time.Second, time.Second, true) if client == nil { t.Fatal("failed to create leaderelection client") } - client.RegisterOnNew(func(currentID, lockID string) {}) + var isLeader bool + client.RegisterOnNew(func(currentID, lockID string) { + isLeader = currentID == lockID + }) + client.RegisterOnStart(func(c context.Context) {}) client.RegisterOnStop(func() {}) lock := client.CreateLock() - if lock.LeaseMeta.Name != "policy-reporter" { - t.Error("unexpected lease name") - } - if lock.LeaseMeta.Namespace != "namespace" { - t.Error("unexpected lease namespace") - } - if lock.LockConfig.Identity != "pod-123" { - t.Error("unexpected lease identity") - } + assert.Equal(t, "policy-reporter", lock.LeaseMeta.Name) + assert.Equal(t, "namespace", lock.LeaseMeta.Namespace) + assert.Equal(t, "pod-123", lock.LockConfig.Identity) + + assert.False(t, isLeader) + + config := client.CreateConfig() + + config.Callbacks.OnNewLeader("pod-123") + + assert.True(t, isLeader) + assert.Equal(t, time.Second, config.LeaseDuration) + assert.Equal(t, time.Second, config.RenewDeadline) + assert.Equal(t, time.Second, config.RetryPeriod) + assert.True(t, config.ReleaseOnCancel) } diff --git a/pkg/listener/cleanup.go b/pkg/listener/cleanup.go index 2dfbd6ee..5206b627 100644 --- a/pkg/listener/cleanup.go +++ b/pkg/listener/cleanup.go @@ -9,9 +9,13 @@ import ( const CleanUpListener = "cleanup_listener" -func NewCleanupListener(ctx context.Context, handlers []target.Client) report.PolicyReportListener { +func NewCleanupListener(ctx context.Context, targets *target.Collection) report.PolicyReportListener { return func(event report.LifecycleEvent) { - for _, handler := range handlers { + if event.Type == report.Added { + return + } + + for _, handler := range targets.SyncClients() { handler.CleanUp(ctx, event.PolicyReport) } } diff --git a/pkg/listener/cleanup_test.go b/pkg/listener/cleanup_test.go index bc7fa7d3..d2b7656c 100644 --- a/pkg/listener/cleanup_test.go +++ b/pkg/listener/cleanup_test.go @@ -3,6 +3,8 @@ package listener_test import ( "testing" + "github.com/stretchr/testify/assert" + "github.com/kyverno/policy-reporter/pkg/listener" "github.com/kyverno/policy-reporter/pkg/report" "github.com/kyverno/policy-reporter/pkg/target" @@ -10,13 +12,11 @@ import ( func Test_CleanupListener(t *testing.T) { t.Run("Execute Cleanup Handler", func(t *testing.T) { - c := &client{} + c := &client{cleanup: true} - slistener := listener.NewCleanupListener(ctx, []target.Client{c}) - slistener(report.LifecycleEvent{Type: report.Added, PolicyReport: preport1}) + slistener := listener.NewCleanupListener(ctx, target.NewCollection(&target.Target{Client: c})) + slistener(report.LifecycleEvent{Type: report.Deleted, PolicyReport: preport1}) - if !c.cleanupCalled { - t.Error("expected cleanup method was called") - } + assert.True(t, c.cleanupCalled, "expected cleanup method was called") }) } diff --git a/pkg/listener/metrics.go b/pkg/listener/metrics.go index fa870ab9..c67f3a51 100644 --- a/pkg/listener/metrics.go +++ b/pkg/listener/metrics.go @@ -36,7 +36,7 @@ func NewMetricsListener( func ResultListeners( filter *report.ResultFilter, - reportFilter *report.ReportFilter, + _ *report.ReportFilter, mode metrics.Mode, labels []string, ) []report.PolicyReportListener { @@ -96,20 +96,8 @@ func ResultListeners( } } - prCallback := metrics.CreateDetailedResultMetricListener(filter, metrics.RegisterDetailedResultGauge(ResultGaugeName)) - pCallback := metrics.CreatePolicyReportMetricsListener(reportFilter) - - crCallback := metrics.CreateDetailedClusterResultMetricListener(filter, metrics.RegisterDetailedClusterResultGauge(ClusterResultGaugeName)) - cCallback := metrics.CreateClusterPolicyReportMetricsListener(reportFilter) - return []report.PolicyReportListener{ - func(event report.LifecycleEvent) { - pCallback(event) - prCallback(event) - }, - func(event report.LifecycleEvent) { - cCallback(event) - crCallback(event) - }, + metrics.CreateDetailedResultMetricListener(filter, metrics.RegisterDetailedResultGauge(ResultGaugeName)), + metrics.CreateDetailedClusterResultMetricListener(filter, metrics.RegisterDetailedClusterResultGauge(ClusterResultGaugeName)), } } diff --git a/pkg/listener/metrics/cluster_policy_report.go b/pkg/listener/metrics/cluster_policy_report.go deleted file mode 100644 index e80daf9c..00000000 --- a/pkg/listener/metrics/cluster_policy_report.go +++ /dev/null @@ -1,50 +0,0 @@ -package metrics - -import ( - "strings" - - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" - - "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" - "github.com/kyverno/policy-reporter/pkg/report" -) - -var clusterPolicyGauge = promauto.NewGaugeVec(prometheus.GaugeOpts{ - Name: "cluster_policy_report_summary", - Help: "Summary of all ClusterPolicyReports", -}, []string{"name", "status"}) - -func CreateClusterPolicyReportMetricsListener(filter *report.ReportFilter) report.PolicyReportListener { - prometheus.Register(clusterPolicyGauge) - - var newReport v1alpha2.ReportInterface - - return func(event report.LifecycleEvent) { - newReport = event.PolicyReport - if !filter.Validate(newReport) { - return - } - - switch event.Type { - case report.Added: - clusterPolicyGauge.WithLabelValues(newReport.GetName(), strings.Title(v1alpha2.StatusSkip)).Set(float64(newReport.GetSummary().Skip)) - clusterPolicyGauge.WithLabelValues(newReport.GetName(), strings.Title(v1alpha2.StatusPass)).Set(float64(newReport.GetSummary().Pass)) - clusterPolicyGauge.WithLabelValues(newReport.GetName(), strings.Title(v1alpha2.StatusWarn)).Set(float64(newReport.GetSummary().Warn)) - clusterPolicyGauge.WithLabelValues(newReport.GetName(), strings.Title(v1alpha2.StatusFail)).Set(float64(newReport.GetSummary().Fail)) - clusterPolicyGauge.WithLabelValues(newReport.GetName(), strings.Title(v1alpha2.StatusError)).Set(float64(newReport.GetSummary().Error)) - case report.Updated: - clusterPolicyGauge.WithLabelValues(newReport.GetName(), strings.Title(v1alpha2.StatusSkip)).Set(float64(newReport.GetSummary().Skip)) - clusterPolicyGauge.WithLabelValues(newReport.GetName(), strings.Title(v1alpha2.StatusPass)).Set(float64(newReport.GetSummary().Pass)) - clusterPolicyGauge.WithLabelValues(newReport.GetName(), strings.Title(v1alpha2.StatusWarn)).Set(float64(newReport.GetSummary().Warn)) - clusterPolicyGauge.WithLabelValues(newReport.GetName(), strings.Title(v1alpha2.StatusFail)).Set(float64(newReport.GetSummary().Fail)) - clusterPolicyGauge.WithLabelValues(newReport.GetName(), strings.Title(v1alpha2.StatusError)).Set(float64(newReport.GetSummary().Error)) - case report.Deleted: - clusterPolicyGauge.DeleteLabelValues(newReport.GetName(), strings.Title(v1alpha2.StatusSkip)) - clusterPolicyGauge.DeleteLabelValues(newReport.GetName(), strings.Title(v1alpha2.StatusPass)) - clusterPolicyGauge.DeleteLabelValues(newReport.GetName(), strings.Title(v1alpha2.StatusWarn)) - clusterPolicyGauge.DeleteLabelValues(newReport.GetName(), strings.Title(v1alpha2.StatusFail)) - clusterPolicyGauge.DeleteLabelValues(newReport.GetName(), strings.Title(v1alpha2.StatusError)) - } - } -} diff --git a/pkg/listener/metrics/cluster_policy_report_test.go b/pkg/listener/metrics/cluster_policy_report_test.go deleted file mode 100644 index c10e29e1..00000000 --- a/pkg/listener/metrics/cluster_policy_report_test.go +++ /dev/null @@ -1,173 +0,0 @@ -package metrics_test - -import ( - "fmt" - "testing" - - "github.com/prometheus/client_golang/prometheus" - ioprometheusclient "github.com/prometheus/client_model/go" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" - "github.com/kyverno/policy-reporter/pkg/listener/metrics" - "github.com/kyverno/policy-reporter/pkg/report" - "github.com/kyverno/policy-reporter/pkg/validate" -) - -func Test_ClusterPolicyReportMetricGeneration(t *testing.T) { - report1 := &v1alpha2.PolicyReport{ - ObjectMeta: v1.ObjectMeta{ - Name: "cpolr-test", - CreationTimestamp: v1.Now(), - }, - Summary: v1alpha2.PolicyReportSummary{Pass: 1, Fail: 1}, - } - - report2 := &v1alpha2.PolicyReport{ - ObjectMeta: v1.ObjectMeta{ - Name: "cpolr-test", - CreationTimestamp: v1.Now(), - }, - Summary: v1alpha2.PolicyReportSummary{Pass: 0, Fail: 1}, - } - - report3 := &v1alpha2.PolicyReport{ - ObjectMeta: v1.ObjectMeta{ - Name: "cpolr-test", - CreationTimestamp: v1.Now(), - }, - Summary: v1alpha2.PolicyReportSummary{Pass: 0, Fail: 1}, - Results: []v1alpha2.PolicyReportResult{{Source: "Kube Bench"}}, - } - - filter := metrics.NewReportFilter(validate.RuleSets{}, validate.RuleSets{Exclude: []string{"Kube Bench"}}) - handler := metrics.CreateClusterPolicyReportMetricsListener(filter) - - t.Run("Added Metric", func(t *testing.T) { - handler(report.LifecycleEvent{Type: report.Added, PolicyReport: report1}) - - metricFam, err := prometheus.DefaultGatherer.Gather() - if err != nil { - t.Errorf("unexpected Error: %s", err) - } - - summary := findMetric(metricFam, "cluster_policy_report_summary") - if summary == nil { - t.Fatalf("Metric not found: cluster_policy_report_summary") - } - - metrics := summary.GetMetric() - - if err = testClusterSummaryMetricLabels(metrics[0], report1, "Error", 0); err != nil { - t.Error(err) - } - if err = testClusterSummaryMetricLabels(metrics[1], report1, "Fail", 1); err != nil { - t.Error(err) - } - if err = testClusterSummaryMetricLabels(metrics[2], report1, "Pass", 1); err != nil { - t.Error(err) - } - if err = testClusterSummaryMetricLabels(metrics[3], report1, "Skip", 0); err != nil { - t.Error(err) - } - if err = testClusterSummaryMetricLabels(metrics[4], report1, "Warn", 0); err != nil { - t.Error(err) - } - }) - - t.Run("Modified Metric", func(t *testing.T) { - handler(report.LifecycleEvent{Type: report.Added, PolicyReport: report1}) - handler(report.LifecycleEvent{Type: report.Updated, PolicyReport: report2}) - - metricFam, err := prometheus.DefaultGatherer.Gather() - if err != nil { - t.Errorf("unexpected Error: %s", err) - } - - summary := findMetric(metricFam, "cluster_policy_report_summary") - if summary == nil { - t.Fatalf("Metric not found: cluster_policy_report_summary") - } - - metrics := summary.GetMetric() - - if err = testClusterSummaryMetricLabels(metrics[0], report2, "Error", 0); err != nil { - t.Error(err) - } - if err = testClusterSummaryMetricLabels(metrics[1], report2, "Fail", 1); err != nil { - t.Error(err) - } - if err = testClusterSummaryMetricLabels(metrics[2], report2, "Pass", 0); err != nil { - t.Error(err) - } - if err = testClusterSummaryMetricLabels(metrics[3], report2, "Skip", 0); err != nil { - t.Error(err) - } - if err = testClusterSummaryMetricLabels(metrics[4], report2, "Warn", 0); err != nil { - t.Error(err) - } - }) - - t.Run("Deleted Metric", func(t *testing.T) { - handler(report.LifecycleEvent{Type: report.Added, PolicyReport: report1}) - handler(report.LifecycleEvent{Type: report.Updated, PolicyReport: report2}) - handler(report.LifecycleEvent{Type: report.Deleted, PolicyReport: report2}) - - metricFam, err := prometheus.DefaultGatherer.Gather() - if err != nil { - t.Errorf("unexpected Error: %s", err) - } - - summary := findMetric(metricFam, "cluster_policy_report_summary") - if summary != nil { - t.Error("cluster_policy_report_summary should no longer exist", *summary.Name) - } - - results := metricFam[0] - - if *results.Name == "cluster_policy_report_result" { - t.Error("cluster_policy_report_result should no longer exist", *results.Name) - } - }) - - t.Run("Filtered Report", func(t *testing.T) { - handler(report.LifecycleEvent{Type: report.Added, PolicyReport: report3}) - - metricFam, err := prometheus.DefaultGatherer.Gather() - if err != nil { - t.Errorf("unexpected Error: %s", err) - } - - summary := findMetric(metricFam, "cluster_policy_report_summary") - if summary != nil { - t.Error("cluster_policy_report_summary should not be created", *summary.Name) - } - }) -} - -func testClusterSummaryMetricLabels( - metric *ioprometheusclient.Metric, - preport v1alpha2.ReportInterface, - status string, - gauge float64, -) error { - if name := *metric.Label[0].Name; name != "name" { - return fmt.Errorf("unexpected Name Label: %s", name) - } - if value := *metric.Label[0].Value; value != preport.GetName() { - return fmt.Errorf("unexpected Name Label Value: %s", value) - } - - if name := *metric.Label[1].Name; name != "status" { - return fmt.Errorf("unexpected Name Label: %s", name) - } - if value := *metric.Label[1].Value; value != status { - return fmt.Errorf("unexpected Status Label Value: %s", value) - } - - if value := metric.Gauge.GetValue(); value != gauge { - return fmt.Errorf("unexpected Metric Value: %v", value) - } - - return nil -} diff --git a/pkg/listener/metrics/policy_report.go b/pkg/listener/metrics/policy_report.go deleted file mode 100644 index 7960ab45..00000000 --- a/pkg/listener/metrics/policy_report.go +++ /dev/null @@ -1,50 +0,0 @@ -package metrics - -import ( - "strings" - - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" - - "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" - "github.com/kyverno/policy-reporter/pkg/report" -) - -var policyGauge = promauto.NewGaugeVec(prometheus.GaugeOpts{ - Name: "policy_report_summary", - Help: "Summary of all PolicyReports", -}, []string{"namespace", "name", "status"}) - -func CreatePolicyReportMetricsListener(filter *report.ReportFilter) report.PolicyReportListener { - prometheus.Register(policyGauge) - - var newReport v1alpha2.ReportInterface - - return func(event report.LifecycleEvent) { - newReport = event.PolicyReport - if !filter.Validate(newReport) { - return - } - - switch event.Type { - case report.Added: - policyGauge.WithLabelValues(newReport.GetNamespace(), newReport.GetName(), strings.Title(v1alpha2.StatusSkip)).Set(float64(newReport.GetSummary().Skip)) - policyGauge.WithLabelValues(newReport.GetNamespace(), newReport.GetName(), strings.Title(v1alpha2.StatusPass)).Set(float64(newReport.GetSummary().Pass)) - policyGauge.WithLabelValues(newReport.GetNamespace(), newReport.GetName(), strings.Title(v1alpha2.StatusWarn)).Set(float64(newReport.GetSummary().Warn)) - policyGauge.WithLabelValues(newReport.GetNamespace(), newReport.GetName(), strings.Title(v1alpha2.StatusFail)).Set(float64(newReport.GetSummary().Fail)) - policyGauge.WithLabelValues(newReport.GetNamespace(), newReport.GetName(), strings.Title(v1alpha2.StatusError)).Set(float64(newReport.GetSummary().Error)) - case report.Updated: - policyGauge.WithLabelValues(newReport.GetNamespace(), newReport.GetName(), strings.Title(v1alpha2.StatusSkip)).Set(float64(newReport.GetSummary().Skip)) - policyGauge.WithLabelValues(newReport.GetNamespace(), newReport.GetName(), strings.Title(v1alpha2.StatusPass)).Set(float64(newReport.GetSummary().Pass)) - policyGauge.WithLabelValues(newReport.GetNamespace(), newReport.GetName(), strings.Title(v1alpha2.StatusWarn)).Set(float64(newReport.GetSummary().Warn)) - policyGauge.WithLabelValues(newReport.GetNamespace(), newReport.GetName(), strings.Title(v1alpha2.StatusFail)).Set(float64(newReport.GetSummary().Fail)) - policyGauge.WithLabelValues(newReport.GetNamespace(), newReport.GetName(), strings.Title(v1alpha2.StatusError)).Set(float64(newReport.GetSummary().Error)) - case report.Deleted: - policyGauge.DeleteLabelValues(newReport.GetNamespace(), newReport.GetName(), strings.Title(v1alpha2.StatusSkip)) - policyGauge.DeleteLabelValues(newReport.GetNamespace(), newReport.GetName(), strings.Title(v1alpha2.StatusPass)) - policyGauge.DeleteLabelValues(newReport.GetNamespace(), newReport.GetName(), strings.Title(v1alpha2.StatusWarn)) - policyGauge.DeleteLabelValues(newReport.GetNamespace(), newReport.GetName(), strings.Title(v1alpha2.StatusFail)) - policyGauge.DeleteLabelValues(newReport.GetNamespace(), newReport.GetName(), strings.Title(v1alpha2.StatusError)) - } - } -} diff --git a/pkg/listener/metrics/policy_report_test.go b/pkg/listener/metrics/policy_report_test.go deleted file mode 100644 index ccacfe11..00000000 --- a/pkg/listener/metrics/policy_report_test.go +++ /dev/null @@ -1,143 +0,0 @@ -package metrics_test - -import ( - "testing" - - "github.com/prometheus/client_golang/prometheus" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" - "github.com/kyverno/policy-reporter/pkg/listener/metrics" - "github.com/kyverno/policy-reporter/pkg/report" - "github.com/kyverno/policy-reporter/pkg/validate" -) - -func Test_PolicyReportMetricGeneration(t *testing.T) { - report1 := &v1alpha2.PolicyReport{ - ObjectMeta: v1.ObjectMeta{ - Name: "polr-test", - Namespace: "test", - CreationTimestamp: v1.Now(), - }, - Summary: v1alpha2.PolicyReportSummary{Pass: 2, Fail: 1}, - } - - report2 := &v1alpha2.PolicyReport{ - ObjectMeta: v1.ObjectMeta{ - Name: "polr-test", - Namespace: "test", - CreationTimestamp: v1.Now(), - }, - Summary: v1alpha2.PolicyReportSummary{Pass: 3, Fail: 4}, - } - - report3 := &v1alpha2.PolicyReport{ - ObjectMeta: v1.ObjectMeta{ - Name: "polr-dev", - Namespace: "dev", - CreationTimestamp: v1.Now(), - }, - Summary: v1alpha2.PolicyReportSummary{Pass: 0, Fail: 1, Warn: 3}, - } - - filter := metrics.NewReportFilter(validate.RuleSets{Exclude: []string{"dev"}}, validate.RuleSets{Exclude: []string{"Test"}}) - - t.Run("Added Metric", func(t *testing.T) { - handler := metrics.CreatePolicyReportMetricsListener(filter) - handler(report.LifecycleEvent{Type: report.Added, PolicyReport: report1}) - - metricFam, err := prometheus.DefaultGatherer.Gather() - if err != nil { - t.Errorf("unexpected Error: %s", err) - } - - summary := findMetric(metricFam, "policy_report_summary") - if summary == nil { - t.Fatalf("Metric not found: policy_report_summary") - } - - metrics := summary.GetMetric() - - if err = testSummaryMetricLabels(metrics[0], report1, "Error", 0); err != nil { - t.Error(err) - } - if err = testSummaryMetricLabels(metrics[1], report1, "Fail", 1); err != nil { - t.Error(err) - } - if err = testSummaryMetricLabels(metrics[2], report1, "Pass", 2); err != nil { - t.Error(err) - } - if err = testSummaryMetricLabels(metrics[3], report1, "Skip", 0); err != nil { - t.Error(err) - } - if err = testSummaryMetricLabels(metrics[4], report1, "Warn", 0); err != nil { - t.Error(err) - } - }) - - t.Run("Modified Metric", func(t *testing.T) { - handler := metrics.CreatePolicyReportMetricsListener(filter) - handler(report.LifecycleEvent{Type: report.Added, PolicyReport: report1}) - handler(report.LifecycleEvent{Type: report.Updated, PolicyReport: report2}) - - metricFam, err := prometheus.DefaultGatherer.Gather() - if err != nil { - t.Errorf("unexpected Error: %s", err) - } - - summary := findMetric(metricFam, "policy_report_summary") - if summary == nil { - t.Fatalf("Metric not found: policy_report_summary") - } - - metrics := summary.GetMetric() - - if err = testSummaryMetricLabels(metrics[0], preport, "Error", 0); err != nil { - t.Error(err) - } - if err = testSummaryMetricLabels(metrics[1], preport, "Fail", 4); err != nil { - t.Error(err) - } - if err = testSummaryMetricLabels(metrics[2], preport, "Pass", 3); err != nil { - t.Error(err) - } - if err = testSummaryMetricLabels(metrics[3], preport, "Skip", 0); err != nil { - t.Error(err) - } - if err = testSummaryMetricLabels(metrics[4], preport, "Warn", 0); err != nil { - t.Error(err) - } - }) - - t.Run("Deleted Metric", func(t *testing.T) { - handler := metrics.CreatePolicyReportMetricsListener(filter) - handler(report.LifecycleEvent{Type: report.Added, PolicyReport: report1}) - handler(report.LifecycleEvent{Type: report.Updated, PolicyReport: report2}) - handler(report.LifecycleEvent{Type: report.Deleted, PolicyReport: report2}) - - metricFam, err := prometheus.DefaultGatherer.Gather() - if err != nil { - t.Errorf("unexpected Error: %s", err) - } - - summary := findMetric(metricFam, "policy_report_summary") - if summary != nil { - t.Error("policy_report_summary should no longer exist", *summary.Name) - } - }) - - t.Run("Validate Metric Filter", func(t *testing.T) { - handler := metrics.CreatePolicyReportMetricsListener(filter) - handler(report.LifecycleEvent{Type: report.Added, PolicyReport: report3}) - - metricFam, err := prometheus.DefaultGatherer.Gather() - if err != nil { - t.Errorf("unexpected Error: %s", err) - } - - summary := findMetric(metricFam, "policy_report_summary") - if summary != nil { - t.Error("policy_report_summary should not created", *summary.Name) - } - }) -} diff --git a/pkg/listener/metrics_test.go b/pkg/listener/metrics_test.go index 9a74f3c4..5d0e2ce1 100644 --- a/pkg/listener/metrics_test.go +++ b/pkg/listener/metrics_test.go @@ -5,6 +5,7 @@ import ( "github.com/prometheus/client_golang/prometheus" ioprometheusclient "github.com/prometheus/client_model/go" + "github.com/stretchr/testify/assert" "github.com/kyverno/policy-reporter/pkg/listener" "github.com/kyverno/policy-reporter/pkg/listener/metrics" @@ -26,13 +27,10 @@ func Test_SimpleMetricsListener(t *testing.T) { } summary := findMetric(metricFam, "cluster_policy_report_summary") - if summary != nil { - t.Fatalf("Metric should not be created: cluster_policy_report_summary") - } + assert.Nil(t, summary, "Metric should not be created: cluster_policy_report_summary") + result := findMetric(metricFam, "cluster_policy_report_simple_result") - if result == nil { - t.Fatalf("Metric not found: cluster_policy_report_simple_result") - } + assert.NotNil(t, result, "Metric not found: cluster_policy_report_simple_result") }) t.Run("Add PolicyReport Metric", func(t *testing.T) { slistener(report.LifecycleEvent{Type: report.Added, PolicyReport: preport1}) @@ -43,13 +41,10 @@ func Test_SimpleMetricsListener(t *testing.T) { } summary := findMetric(metricFam, "policy_report_summary") - if summary != nil { - t.Fatalf("Metric should not be created: policy_report_summary") - } + assert.Nil(t, summary, "Metric should not be created: policy_report_summary") + result := findMetric(metricFam, "policy_report_simple_result") - if result == nil { - t.Fatalf("Metric not found: policy_report_simple_result") - } + assert.NotNil(t, result, "Metric not found: policy_report_simple_result") }) } @@ -69,13 +64,10 @@ func Test_CustomMetricsListener(t *testing.T) { } summary := findMetric(metricFam, "cluster_policy_report_summary") - if summary != nil { - t.Fatalf("Metric should not be created: cluster_policy_report_summary") - } + assert.Nil(t, summary, "Metric should not be created: cluster_policy_report_summary") + result := findMetric(metricFam, "cluster_policy_report_custom_result") - if result == nil { - t.Fatalf("Metric not found: cluster_policy_report_custom_result") - } + assert.NotNil(t, result, "Metric not found: cluster_policy_report_custom_result") }) t.Run("Add PolicyReport Metric", func(t *testing.T) { slistener(report.LifecycleEvent{Type: report.Added, PolicyReport: preport1}) @@ -86,13 +78,10 @@ func Test_CustomMetricsListener(t *testing.T) { } summary := findMetric(metricFam, "policy_report_summary") - if summary != nil { - t.Fatalf("Metric should not be created: policy_report_summary") - } + assert.Nil(t, summary, "Metric should not be created: policy_report_summary") + result := findMetric(metricFam, "policy_report_custom_result") - if result == nil { - t.Fatalf("Metric not found: policy_report_custom_result") - } + assert.NotNil(t, result, "Metric not found: policy_report_custom_result") }) } @@ -110,14 +99,8 @@ func Test_MetricsListener(t *testing.T) { t.Errorf("unexpected Error: %s", err) } - summary := findMetric(metricFam, "cluster_policy_report_summary") - if summary == nil { - t.Fatalf("Metric not found: cluster_policy_report_summary") - } result := findMetric(metricFam, "cluster_policy_report_result") - if result == nil { - t.Fatalf("Metric not found: cluster_policy_report_result") - } + assert.NotNil(t, result, "Metric not found: cluster_policy_report_result") }) t.Run("Add PolicyReport Metric", func(t *testing.T) { slistener(report.LifecycleEvent{Type: report.Added, PolicyReport: preport1}) @@ -127,14 +110,8 @@ func Test_MetricsListener(t *testing.T) { t.Errorf("unexpected Error: %s", err) } - summary := findMetric(metricFam, "policy_report_summary") - if summary == nil { - t.Fatalf("Metric not found: policy_report_summary") - } result := findMetric(metricFam, "policy_report_result") - if result == nil { - t.Fatalf("Metric not found: policy_report_result") - } + assert.NotNil(t, result, "Metric not found: policy_report_result") }) } diff --git a/pkg/listener/new_result.go b/pkg/listener/new_result.go index 0a72cd00..ba2a77dd 100644 --- a/pkg/listener/new_result.go +++ b/pkg/listener/new_result.go @@ -13,10 +13,12 @@ import ( const NewResults = "new_results_listener" type ResultListener struct { - skipExisting bool - listener []report.PolicyReportResultListener - cache cache.Cache - startUp time.Time + skipExisting bool + listener []report.PolicyReportResultListener + scopeListener []report.ScopeResultsListener + syncListener []report.SyncResultsListener + cache cache.Cache + startUp time.Time } func (l *ResultListener) RegisterListener(listener report.PolicyReportResultListener) { @@ -27,6 +29,30 @@ func (l *ResultListener) UnregisterListener() { l.listener = make([]report.PolicyReportResultListener, 0) } +func (l *ResultListener) RegisterScopeListener(listener report.ScopeResultsListener) { + l.scopeListener = append(l.scopeListener, listener) +} + +func (l *ResultListener) UnregisterScopeListener() { + l.scopeListener = make([]report.ScopeResultsListener, 0) +} + +func (l *ResultListener) RegisterSyncListener(listener report.SyncResultsListener) { + l.syncListener = append(l.syncListener, listener) +} + +func (l *ResultListener) UnregisterSyncListener() { + l.syncListener = make([]report.SyncResultsListener, 0) +} + +func (l *ResultListener) Validate(r v1alpha2.PolicyReportResult) bool { + if r.Result == v1alpha2.StatusSkip || r.Result == v1alpha2.StatusPass { + return false + } + + return true +} + func (l *ResultListener) Listen(event report.LifecycleEvent) { if event.Type != report.Added && event.Type != report.Updated { l.cache.RemoveReport(event.PolicyReport.GetID()) @@ -39,7 +65,25 @@ func (l *ResultListener) Listen(event report.LifecycleEvent) { } listenerCount := len(l.listener) - if listenerCount == 0 { + scopeListenerCount := len(l.scopeListener) + syncListenerCount := len(l.syncListener) + + if syncListenerCount > 0 { + wg := sync.WaitGroup{} + wg.Add(syncListenerCount) + + for _, cb := range l.syncListener { + go func(callback report.SyncResultsListener) { + defer wg.Done() + + callback(event.PolicyReport) + }(cb) + } + + wg.Wait() + } + + if listenerCount == 0 && scopeListenerCount == 0 { l.cache.AddReport(event.PolicyReport) return } @@ -56,31 +100,59 @@ func (l *ResultListener) Listen(event report.LifecycleEvent) { } existing := l.cache.GetResults(event.PolicyReport.GetID()) + newResults := make([]v1alpha2.PolicyReportResult, 0) + + for _, r := range event.PolicyReport.GetResults() { + if helper.Contains(r.GetID(), existing) || !l.Validate(r) { + continue + } + + if r.Timestamp.Seconds > 0 { + created := time.Unix(r.Timestamp.Seconds, int64(r.Timestamp.Nanos)) + if l.skipExisting && created.Local().Before(l.startUp) { + continue + } + } + + newResults = append(newResults, r) + } + + l.cache.AddReport(event.PolicyReport) + if len(newResults) == 0 { + return + } + + if scopeListenerCount > 0 { + wg := sync.WaitGroup{} + wg.Add(scopeListenerCount) + + for _, cb := range l.scopeListener { + go func(callback report.ScopeResultsListener, results []v1alpha2.PolicyReportResult) { + defer wg.Done() + + callback(event.PolicyReport, results, preExisted) + }(cb, newResults) + } + } + + if len(l.listener) == 0 { + return + } grp := sync.WaitGroup{} - grp.Add(resultCount) - for _, res := range event.PolicyReport.GetResults() { + grp.Add(len(newResults)) + for _, res := range newResults { go func(r v1alpha2.PolicyReportResult) { defer grp.Done() - if helper.Contains(r.GetID(), existing) { - return - } - - if r.Timestamp.Seconds > 0 { - created := time.Unix(r.Timestamp.Seconds, int64(r.Timestamp.Nanos)) - if l.skipExisting && created.Local().Before(l.startUp) { - return - } - } - wg := sync.WaitGroup{} wg.Add(listenerCount) for _, cb := range l.listener { go func(callback report.PolicyReportResultListener, result v1alpha2.PolicyReportResult) { + defer wg.Done() + callback(event.PolicyReport, result, preExisted) - wg.Done() }(cb, r) } @@ -89,15 +161,15 @@ func (l *ResultListener) Listen(event report.LifecycleEvent) { } grp.Wait() - - l.cache.AddReport(event.PolicyReport) } func NewResultListener(skipExisting bool, rcache cache.Cache, startUp time.Time) *ResultListener { return &ResultListener{ - skipExisting: skipExisting, - cache: rcache, - startUp: startUp, - listener: make([]report.PolicyReportResultListener, 0), + skipExisting: skipExisting, + cache: rcache, + startUp: startUp, + listener: make([]report.PolicyReportResultListener, 0), + scopeListener: make([]report.ScopeResultsListener, 0), + syncListener: make([]report.SyncResultsListener, 0), } } diff --git a/pkg/listener/new_result_test.go b/pkg/listener/new_result_test.go index 3be27c73..73f8dee9 100644 --- a/pkg/listener/new_result_test.go +++ b/pkg/listener/new_result_test.go @@ -4,6 +4,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/kyverno/policy-reporter/pkg/cache" @@ -25,9 +26,7 @@ func Test_ResultListener(t *testing.T) { slistener.Listen(report.LifecycleEvent{Type: report.Added, PolicyReport: preport1}) slistener.Listen(report.LifecycleEvent{Type: report.Updated, PolicyReport: preport2}) - if called.GetID() != fixtures.FailPodResult.GetID() { - t.Error("Expected Listener to be called with FailPodResult") - } + assert.Equal(t, called.GetID(), fixtures.FailPodResult.GetID(), "Expected Listener to be called with FailPodResult") }) t.Run("Ignore Delete Event", func(t *testing.T) { @@ -40,9 +39,7 @@ func Test_ResultListener(t *testing.T) { slistener.Listen(report.LifecycleEvent{Type: report.Deleted, PolicyReport: preport2}) - if called { - t.Error("Expected Listener not be called on Deleted event") - } + assert.False(t, called, "Expected Listener not be called on Deleted event") }) t.Run("Ignore Added Results created before startup", func(t *testing.T) { @@ -55,9 +52,7 @@ func Test_ResultListener(t *testing.T) { slistener.Listen(report.LifecycleEvent{Type: report.Added, PolicyReport: preport2}) - if called { - t.Error("Expected Listener not be called on Deleted event") - } + assert.False(t, called, "Expected Listener not be called on Deleted event") }) t.Run("Ignore CacheResults", func(t *testing.T) { @@ -71,9 +66,7 @@ func Test_ResultListener(t *testing.T) { slistener.Listen(report.LifecycleEvent{Type: report.Added, PolicyReport: preport2}) slistener.Listen(report.LifecycleEvent{Type: report.Updated, PolicyReport: preport2}) - if called { - t.Error("Expected Listener not be called on cached results") - } + assert.False(t, called, "Expected Listener not be called on cached results") }) t.Run("Early Return if Results are empty", func(t *testing.T) { @@ -86,9 +79,7 @@ func Test_ResultListener(t *testing.T) { slistener.Listen(report.LifecycleEvent{Type: report.Updated, PolicyReport: preport3}) - if called { - t.Error("Expected Listener not be called with empty results") - } + assert.False(t, called, "Expected Listener not be called with empty results") }) t.Run("Skip process events when no listeners registered", func(t *testing.T) { @@ -97,9 +88,7 @@ func Test_ResultListener(t *testing.T) { slistener := listener.NewResultListener(true, c, time.Now()) slistener.Listen(report.LifecycleEvent{Type: report.Added, PolicyReport: preport2}) - if res := c.GetResults(preport2.GetID()); len(res) == 0 { - t.Error("Expected cached report was found") - } + assert.Greater(t, len(c.GetResults(preport2.GetID())), 0, "Expected cached report was found") }) t.Run("UnregisterListener removes all listeners", func(t *testing.T) { @@ -114,9 +103,7 @@ func Test_ResultListener(t *testing.T) { slistener.Listen(report.LifecycleEvent{Type: report.Updated, PolicyReport: preport2}) - if called { - t.Error("Expected Listener not called because it was unregistered") - } + assert.False(t, called, "Expected Listener not called because it was unregistered") }) t.Run("ignore results with past timestamps", func(t *testing.T) { var called bool @@ -136,8 +123,6 @@ func Test_ResultListener(t *testing.T) { slistener.Listen(report.LifecycleEvent{Type: report.Updated, PolicyReport: rep}) - if called { - t.Error("Expected Listener not called because it was unregistered") - } + assert.False(t, called, "Expected Listener not called because it was unregistered") }) } diff --git a/pkg/listener/scope_results.go b/pkg/listener/scope_results.go new file mode 100644 index 00000000..5bf92a9f --- /dev/null +++ b/pkg/listener/scope_results.go @@ -0,0 +1,39 @@ +package listener + +import ( + "sync" + + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" + "github.com/kyverno/policy-reporter/pkg/helper" + "github.com/kyverno/policy-reporter/pkg/report" + "github.com/kyverno/policy-reporter/pkg/target" +) + +const SendScopeResults = "send_scope_results_listener" + +func NewSendScopeResultsListener(targets *target.Collection) report.ScopeResultsListener { + return func(rep v1alpha2.ReportInterface, r []v1alpha2.PolicyReportResult, e bool) { + clients := targets.BatchSendClients() + + wg := &sync.WaitGroup{} + wg.Add(len(clients)) + + for _, t := range clients { + go func(target target.Client, re v1alpha2.ReportInterface, results []v1alpha2.PolicyReportResult, preExisted bool) { + defer wg.Done() + + filtered := helper.Filter(results, func(result v1alpha2.PolicyReportResult) bool { + return target.Validate(re, result) + }) + + if len(filtered) == 0 || preExisted && target.SkipExistingOnStartup() { + return + } + + target.BatchSend(re, filtered) + }(t, rep, r, e) + } + + wg.Wait() + } +} diff --git a/pkg/listener/scope_results_test.go b/pkg/listener/scope_results_test.go new file mode 100644 index 00000000..64bc2064 --- /dev/null +++ b/pkg/listener/scope_results_test.go @@ -0,0 +1,38 @@ +package listener_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" + "github.com/kyverno/policy-reporter/pkg/fixtures" + "github.com/kyverno/policy-reporter/pkg/listener" + "github.com/kyverno/policy-reporter/pkg/target" +) + +func Test_ScopeResultsListener(t *testing.T) { + t.Run("Send Results", func(t *testing.T) { + c := &client{validated: true, batchSend: true} + slistener := listener.NewSendScopeResultsListener(target.NewCollection(&target.Target{Client: c})) + slistener(preport1, []v1alpha2.PolicyReportResult{fixtures.FailResult}, false) + + assert.True(t, c.Called, "Expected Send to be called") + }) + t.Run("Don't Send Result when validation fails", func(t *testing.T) { + c := &client{validated: false, batchSend: true} + slistener := listener.NewSendScopeResultsListener(target.NewCollection(&target.Target{Client: c})) + slistener(preport1, []v1alpha2.PolicyReportResult{fixtures.FailResult}, false) + + assert.False(t, c.Called, "Expected Send not to be called") + }) + t.Run("Don't Send pre existing Result when skipExistingOnStartup is true", func(t *testing.T) { + c := &client{skipExistingOnStartup: true, batchSend: true} + slistener := listener.NewSendScopeResultsListener(target.NewCollection(&target.Target{Client: c})) + slistener(preport1, []v1alpha2.PolicyReportResult{fixtures.FailResult}, true) + + if c.Called { + t.Error("Expected Send not to be called") + } + }) +} diff --git a/pkg/listener/send_result.go b/pkg/listener/send_result.go index 37420d9f..367e9158 100644 --- a/pkg/listener/send_result.go +++ b/pkg/listener/send_result.go @@ -12,8 +12,10 @@ import ( const SendResults = "send_results_listener" -func NewSendResultListener(clients []target.Client) report.PolicyReportResultListener { +func NewSendResultListener(targets *target.Collection) report.PolicyReportResultListener { return func(rep v1alpha2.ReportInterface, r v1alpha2.PolicyReportResult, e bool) { + clients := targets.SingleSendClients() + wg := &sync.WaitGroup{} wg.Add(len(clients)) diff --git a/pkg/listener/send_result_test.go b/pkg/listener/send_result_test.go index e0b50028..8fe4ddb1 100644 --- a/pkg/listener/send_result_test.go +++ b/pkg/listener/send_result_test.go @@ -4,6 +4,8 @@ import ( "context" "testing" + "github.com/stretchr/testify/assert" + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" "github.com/kyverno/policy-reporter/pkg/fixtures" "github.com/kyverno/policy-reporter/pkg/listener" @@ -15,14 +17,16 @@ type client struct { skipExistingOnStartup bool validated bool cleanupCalled bool + batchSend bool + cleanup bool } func (c *client) Send(result v1alpha2.PolicyReportResult) { c.Called = true } -func (c *client) MinimumPriority() string { - return v1alpha2.InfoPriority.String() +func (c *client) MinimumSeverity() string { + return v1alpha2.SeverityInfo } func (c *client) Name() string { @@ -41,36 +45,49 @@ func (c client) Validate(rep v1alpha2.ReportInterface, result v1alpha2.PolicyRep return c.validated } +func (c *client) Reset(_ context.Context) error { + return nil +} + func (c *client) CleanUp(_ context.Context, _ v1alpha2.ReportInterface) { c.cleanupCalled = true } +func (c *client) BatchSend(_ v1alpha2.ReportInterface, _ []v1alpha2.PolicyReportResult) { + c.Called = true +} + +func (c *client) Type() target.ClientType { + if c.cleanup { + return target.SyncSend + } + if c.batchSend { + return target.BatchSend + } + + return target.SingleSend +} + func Test_SendResultListener(t *testing.T) { t.Run("Send Result", func(t *testing.T) { c := &client{validated: true} - slistener := listener.NewSendResultListener([]target.Client{c}) + slistener := listener.NewSendResultListener(target.NewCollection(&target.Target{Client: c})) slistener(preport1, fixtures.FailResult, false) - if !c.Called { - t.Error("Expected Send to be called") - } + assert.True(t, c.Called, "Expected Send to be called") }) t.Run("Don't Send Result when validation fails", func(t *testing.T) { c := &client{validated: false} - slistener := listener.NewSendResultListener([]target.Client{c}) + slistener := listener.NewSendResultListener(target.NewCollection(&target.Target{Client: c})) slistener(preport1, fixtures.FailResult, false) - if c.Called { - t.Error("Expected Send not to be called") - } + assert.False(t, c.Called, "Expected Send not to be called") }) t.Run("Don't Send pre existing Result when skipExistingOnStartup is true", func(t *testing.T) { c := &client{skipExistingOnStartup: true} - slistener := listener.NewSendResultListener([]target.Client{c}) + slistener := listener.NewSendResultListener(target.NewCollection(&target.Target{Client: c})) slistener(preport1, fixtures.FailResult, true) - if c.Called { - t.Error("Expected Send not to be called") - } + assert.False(t, c.Called, "Expected Send not to be called") }) } diff --git a/pkg/listener/sync_results.go b/pkg/listener/sync_results.go new file mode 100644 index 00000000..44d435d7 --- /dev/null +++ b/pkg/listener/sync_results.go @@ -0,0 +1,52 @@ +package listener + +import ( + "context" + "sync" + + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" + "github.com/kyverno/policy-reporter/pkg/helper" + "github.com/kyverno/policy-reporter/pkg/report" + "github.com/kyverno/policy-reporter/pkg/target" +) + +const SendSyncResults = "send_sync_results_listener" + +func NewSendSyncResultsListener(targets *target.Collection) report.SyncResultsListener { + ready := make(chan bool) + ok := false + go func() { + ok = targets.Reset(context.Background()) + if ok { + close(ready) + } + }() + + return func(rep v1alpha2.ReportInterface) { + clients := targets.SyncClients() + if len(clients) == 0 { + return + } + + if !ok { + <-ready + } + + wg := &sync.WaitGroup{} + wg.Add(len(clients)) + + for _, t := range clients { + go func(target target.Client, re v1alpha2.ReportInterface) { + defer wg.Done() + + filtered := helper.Filter(re.GetResults(), func(result v1alpha2.PolicyReportResult) bool { + return target.Validate(re, result) + }) + + target.BatchSend(re, filtered) + }(t, rep) + } + + wg.Wait() + } +} diff --git a/pkg/report/client.go b/pkg/report/client.go index f05f5657..babd440f 100644 --- a/pkg/report/client.go +++ b/pkg/report/client.go @@ -10,6 +10,12 @@ type PolicyReportListener = func(LifecycleEvent) // PolicyReportResultListener is called whenever a new PolicyResult comes in type PolicyReportResultListener = func(v1alpha2.ReportInterface, v1alpha2.PolicyReportResult, bool) +// ScopeResultsListener is called whenever a new PolicyReport with a single resource scope and new results comes in +type ScopeResultsListener = func(v1alpha2.ReportInterface, []v1alpha2.PolicyReportResult, bool) + +// SyncResultsListener is called whenever a PolicyReport event comes in +type SyncResultsListener = func(v1alpha2.ReportInterface) + // PolicyReportClient watches for PolicyReport Events and executes registered callback type PolicyReportClient interface { // Run starts the informer and workerqueue diff --git a/pkg/report/filter.go b/pkg/report/filter.go deleted file mode 100644 index 56c3c3de..00000000 --- a/pkg/report/filter.go +++ /dev/null @@ -1,77 +0,0 @@ -package report - -import ( - "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" - "github.com/kyverno/policy-reporter/pkg/validate" -) - -type Namespaced interface { - GetNamespace() string -} - -type Filter struct { - disbaleClusterReports bool - namespace validate.RuleSets -} - -func (f *Filter) DisableClusterReports() bool { - return f.disbaleClusterReports -} - -func (f *Filter) AllowReport(report Namespaced) bool { - return validate.Namespace(report.GetNamespace(), f.namespace) -} - -func NewFilter(disableClusterReports bool, namespace validate.RuleSets) *Filter { - return &Filter{disableClusterReports, namespace} -} - -type ResultValidation = func(v1alpha2.PolicyReportResult) bool - -type ResultFilter struct { - validations []ResultValidation - Sources []string - MinimumPriority string -} - -func (rf *ResultFilter) AddValidation(v ResultValidation) { - rf.validations = append(rf.validations, v) -} - -func (rf *ResultFilter) Validate(result v1alpha2.PolicyReportResult) bool { - for _, validation := range rf.validations { - if !validation(result) { - return false - } - } - - return true -} - -func NewResultFilter() *ResultFilter { - return &ResultFilter{} -} - -type ReportValidation = func(v1alpha2.ReportInterface) bool - -type ReportFilter struct { - validations []ReportValidation -} - -func (rf *ReportFilter) AddValidation(v ReportValidation) { - rf.validations = append(rf.validations, v) -} - -func (rf *ReportFilter) Validate(report v1alpha2.ReportInterface) bool { - for _, validation := range rf.validations { - if !validation(report) { - return false - } - } - - return true -} - -func NewReportFilter() *ReportFilter { - return &ReportFilter{} -} diff --git a/pkg/report/filter_test.go b/pkg/report/filter_test.go deleted file mode 100644 index dbdfb250..00000000 --- a/pkg/report/filter_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package report_test - -import ( - "testing" - - "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" - "github.com/kyverno/policy-reporter/pkg/fixtures" - "github.com/kyverno/policy-reporter/pkg/report" - "github.com/kyverno/policy-reporter/pkg/validate" -) - -func Test_DisableClusterReports(t *testing.T) { - filter := report.NewFilter(true, validate.RuleSets{}) - - if !filter.DisableClusterReports() { - t.Error("Expected EnableClusterReports to return true as configured") - } -} - -func Test_AllowReport(t *testing.T) { - t.Run("Allow ClusterReport", func(t *testing.T) { - filter := report.NewFilter(true, validate.RuleSets{Exclude: []string{"*"}}) - if !filter.AllowReport(creport) { - t.Error("Expected AllowReport returns true if Report is a ClusterPolicyReport without namespace") - } - }) - - t.Run("Allow Report with matching include Namespace", func(t *testing.T) { - filter := report.NewFilter(true, validate.RuleSets{Include: []string{"patch", "te*"}}) - if !filter.AllowReport(preport) { - t.Error("Expected AllowReport returns true if Report namespace matches include pattern") - } - }) - - t.Run("Disallow Report with matching exclude Namespace", func(t *testing.T) { - filter := report.NewFilter(true, validate.RuleSets{Exclude: []string{"patch", "te*"}}) - if filter.AllowReport(preport) { - t.Error("Expected AllowReport returns false if Report namespace matches exclude pattern") - } - }) - - t.Run("Ignores exclude pattern if include namespaces provided", func(t *testing.T) { - filter := report.NewFilter(true, validate.RuleSets{Include: []string{"*"}, Exclude: []string{"te*"}}) - if !filter.AllowReport(preport) { - t.Error("Expected AllowReport returns true because exclude patterns ignored if include patterns provided") - } - }) - - t.Run("Allow Report when no configuration exists", func(t *testing.T) { - filter := report.NewFilter(true, validate.RuleSets{}) - if !filter.AllowReport(preport) { - t.Error("Expected AllowReport returns true if no namespace patterns configured") - } - }) - - t.Run("Disallow Report if no include namespace matches", func(t *testing.T) { - filter := report.NewFilter(true, validate.RuleSets{Include: []string{"patch", "dev"}}) - if filter.AllowReport(preport) { - t.Error("Expected AllowReport returns false if no namespace pattern matches") - } - }) -} - -func Test_ResultFilter(t *testing.T) { - t.Run("don't filter any result without validations", func(t *testing.T) { - filter := report.NewResultFilter() - if !filter.Validate(fixtures.FailResult) { - t.Error("Expected result validates to true") - } - }) - t.Run("filter result with a false validation", func(t *testing.T) { - filter := report.NewResultFilter() - filter.AddValidation(func(r v1alpha2.PolicyReportResult) bool { return false }) - if filter.Validate(fixtures.FailResult) { - t.Error("Expected result validates to false") - } - }) -} - -func Test_ReportFilter(t *testing.T) { - t.Run("don't filter any result without validations", func(t *testing.T) { - filter := report.NewReportFilter() - if !filter.Validate(preport) { - t.Error("Expected result validates to true") - } - }) - t.Run("filter result with a false validation", func(t *testing.T) { - filter := report.NewReportFilter() - filter.AddValidation(func(r v1alpha2.ReportInterface) bool { return false }) - if filter.Validate(preport) { - t.Error("Expected result validates to false") - } - }) -} diff --git a/pkg/report/meta_filter.go b/pkg/report/meta_filter.go new file mode 100644 index 00000000..445b03d1 --- /dev/null +++ b/pkg/report/meta_filter.go @@ -0,0 +1,26 @@ +package report + +import ( + "github.com/kyverno/policy-reporter/pkg/validate" +) + +type Namespaced interface { + GetNamespace() string +} + +type MetaFilter struct { + disbaleClusterReports bool + namespace validate.RuleSets +} + +func (f *MetaFilter) DisableClusterReports() bool { + return f.disbaleClusterReports +} + +func (f *MetaFilter) AllowReport(report Namespaced) bool { + return validate.Namespace(report.GetNamespace(), f.namespace) +} + +func NewMetaFilter(disableClusterReports bool, namespace validate.RuleSets) *MetaFilter { + return &MetaFilter{disableClusterReports, namespace} +} diff --git a/pkg/report/meta_filter_test.go b/pkg/report/meta_filter_test.go new file mode 100644 index 00000000..5d35ce60 --- /dev/null +++ b/pkg/report/meta_filter_test.go @@ -0,0 +1,60 @@ +package report_test + +import ( + "testing" + + "github.com/kyverno/policy-reporter/pkg/report" + "github.com/kyverno/policy-reporter/pkg/validate" +) + +func TestMetaFilter(t *testing.T) { + t.Run("disable cluster reports", func(t *testing.T) { + filter := report.NewMetaFilter(true, validate.RuleSets{}) + + if !filter.DisableClusterReports() { + t.Error("Expected EnableClusterReports to return true as configured") + } + }) + + t.Run("Allow ClusterReport", func(t *testing.T) { + filter := report.NewMetaFilter(true, validate.RuleSets{Exclude: []string{"*"}}) + if !filter.AllowReport(creport) { + t.Error("Expected AllowReport returns true if Report is a ClusterPolicyReport without namespace") + } + }) + + t.Run("Allow Report with matching include Namespace", func(t *testing.T) { + filter := report.NewMetaFilter(true, validate.RuleSets{Include: []string{"patch", "te*"}}) + if !filter.AllowReport(preport) { + t.Error("Expected AllowReport returns true if Report namespace matches include pattern") + } + }) + + t.Run("Disallow Report with matching exclude Namespace", func(t *testing.T) { + filter := report.NewMetaFilter(true, validate.RuleSets{Exclude: []string{"patch", "te*"}}) + if filter.AllowReport(preport) { + t.Error("Expected AllowReport returns false if Report namespace matches exclude pattern") + } + }) + + t.Run("Ignores exclude pattern if include namespaces provided", func(t *testing.T) { + filter := report.NewMetaFilter(true, validate.RuleSets{Include: []string{"*"}, Exclude: []string{"te*"}}) + if !filter.AllowReport(preport) { + t.Error("Expected AllowReport returns true because exclude patterns ignored if include patterns provided") + } + }) + + t.Run("Allow Report when no configuration exists", func(t *testing.T) { + filter := report.NewMetaFilter(true, validate.RuleSets{}) + if !filter.AllowReport(preport) { + t.Error("Expected AllowReport returns true if no namespace patterns configured") + } + }) + + t.Run("Disallow Report if no include namespace matches", func(t *testing.T) { + filter := report.NewMetaFilter(true, validate.RuleSets{Include: []string{"patch", "dev"}}) + if filter.AllowReport(preport) { + t.Error("Expected AllowReport returns false if no namespace pattern matches") + } + }) +} diff --git a/pkg/report/report_filter.go b/pkg/report/report_filter.go new file mode 100644 index 00000000..671ebbe9 --- /dev/null +++ b/pkg/report/report_filter.go @@ -0,0 +1,29 @@ +package report + +import ( + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" +) + +type ReportValidation = func(v1alpha2.ReportInterface) bool + +type ReportFilter struct { + validations []ReportValidation +} + +func (rf *ReportFilter) AddValidation(v ReportValidation) { + rf.validations = append(rf.validations, v) +} + +func (rf *ReportFilter) Validate(report v1alpha2.ReportInterface) bool { + for _, validation := range rf.validations { + if !validation(report) { + return false + } + } + + return true +} + +func NewReportFilter() *ReportFilter { + return &ReportFilter{} +} diff --git a/pkg/report/report_filter_test.go b/pkg/report/report_filter_test.go new file mode 100644 index 00000000..0a2e40f7 --- /dev/null +++ b/pkg/report/report_filter_test.go @@ -0,0 +1,24 @@ +package report_test + +import ( + "testing" + + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" + "github.com/kyverno/policy-reporter/pkg/report" +) + +func Test_ReportFilter(t *testing.T) { + t.Run("don't filter any result without validations", func(t *testing.T) { + filter := report.NewReportFilter() + if !filter.Validate(preport) { + t.Error("Expected result validates to true") + } + }) + t.Run("filter result with a false validation", func(t *testing.T) { + filter := report.NewReportFilter() + filter.AddValidation(func(r v1alpha2.ReportInterface) bool { return false }) + if filter.Validate(preport) { + t.Error("Expected result validates to false") + } + }) +} diff --git a/pkg/report/result/id_generator.go b/pkg/report/result/id_generator.go index 77d91f0a..8157adc9 100644 --- a/pkg/report/result/id_generator.go +++ b/pkg/report/result/id_generator.go @@ -4,9 +4,10 @@ import ( "strconv" "strings" - "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" "github.com/segmentio/fasthash/fnv1a" corev1 "k8s.io/api/core/v1" + + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" ) type FieldMapperFunc = func(h1 uint64, polr v1alpha2.ReportInterface, res v1alpha2.PolicyReportResult) uint64 diff --git a/pkg/report/result/id_generator_test.go b/pkg/report/result/id_generator_test.go index 0bb8bbad..b0d4a307 100644 --- a/pkg/report/result/id_generator_test.go +++ b/pkg/report/result/id_generator_test.go @@ -3,10 +3,11 @@ package result_test import ( "testing" - "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" - "github.com/kyverno/policy-reporter/pkg/report/result" corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" + "github.com/kyverno/policy-reporter/pkg/report/result" ) func TestDefaultGenerator(t *testing.T) { diff --git a/pkg/report/result/mapper.go b/pkg/report/result/mapper.go deleted file mode 100644 index b2290788..00000000 --- a/pkg/report/result/mapper.go +++ /dev/null @@ -1,29 +0,0 @@ -package result - -import ( - "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" -) - -func ResolvePriority(result v1alpha2.PolicyReportResult) v1alpha2.Priority { - if result.Result == v1alpha2.StatusSkip { - return v1alpha2.DebugPriority - } - - if result.Result == v1alpha2.StatusPass { - return v1alpha2.InfoPriority - } - - if result.Result == v1alpha2.StatusError { - return v1alpha2.ErrorPriority - } - - if result.Result == v1alpha2.StatusWarn { - return v1alpha2.WarningPriority - } - - if result.Severity != "" { - return v1alpha2.PriorityFromSeverity(result.Severity) - } - - return v1alpha2.WarningPriority -} diff --git a/pkg/report/result/mapper_test.go b/pkg/report/result/mapper_test.go deleted file mode 100644 index 143acfb0..00000000 --- a/pkg/report/result/mapper_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package result_test - -import ( - "testing" - - "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" - "github.com/kyverno/policy-reporter/pkg/report/result" -) - -func Test_ResolvePriority(t *testing.T) { - t.Run("Status Skip", func(t *testing.T) { - priority := result.ResolvePriority(v1alpha2.PolicyReportResult{ - Result: v1alpha2.StatusSkip, - Severity: v1alpha2.SeverityHigh, - }) - - if priority != v1alpha2.DebugPriority { - t.Errorf("expected priority debug, got %s", priority.String()) - } - }) - - t.Run("Status Pass", func(t *testing.T) { - priority := result.ResolvePriority(v1alpha2.PolicyReportResult{ - Result: v1alpha2.StatusPass, - Severity: v1alpha2.SeverityHigh, - }) - - if priority != v1alpha2.InfoPriority { - t.Errorf("expected priority info, got %s", priority.String()) - } - }) - - t.Run("Status Warning", func(t *testing.T) { - priority := result.ResolvePriority(v1alpha2.PolicyReportResult{ - Result: v1alpha2.StatusWarn, - Severity: v1alpha2.SeverityHigh, - }) - - if priority != v1alpha2.WarningPriority { - t.Errorf("expected priority warning, got %s", priority.String()) - } - }) - - t.Run("Status Error", func(t *testing.T) { - priority := result.ResolvePriority(v1alpha2.PolicyReportResult{ - Result: v1alpha2.StatusError, - Severity: v1alpha2.SeverityHigh, - }) - - if priority != v1alpha2.ErrorPriority { - t.Errorf("expected priority warning, got %s", priority.String()) - } - }) - - t.Run("Status Fail Fallback", func(t *testing.T) { - priority := result.ResolvePriority(v1alpha2.PolicyReportResult{ - Result: v1alpha2.StatusFail, - }) - - if priority != v1alpha2.WarningPriority { - t.Errorf("expected priority warning as fail fallback, got %s", priority.String()) - } - }) - - t.Run("Status Severity", func(t *testing.T) { - priority := result.ResolvePriority(v1alpha2.PolicyReportResult{ - Result: v1alpha2.StatusFail, - Severity: v1alpha2.SeverityCritical, - }) - - if priority != v1alpha2.CriticalPriority { - t.Errorf("expected priority critical, got %s", priority.String()) - } - }) -} diff --git a/pkg/report/result/reconditioner.go b/pkg/report/result/reconditioner.go index cb1c3ec2..be9bb307 100644 --- a/pkg/report/result/reconditioner.go +++ b/pkg/report/result/reconditioner.go @@ -21,7 +21,6 @@ func (r *Reconditioner) Prepare(polr v1alpha2.ReportInterface) v1alpha2.ReportIn results := polr.GetResults() for i, r := range results { r.ID = generator.Generate(polr, r) - r.Priority = ResolvePriority(r) r.Category = helper.Defaults(r.Category, "Other") scope := polr.GetScope() diff --git a/pkg/report/result/reconditioner_test.go b/pkg/report/result/reconditioner_test.go index 9ad52ff9..97287db6 100644 --- a/pkg/report/result/reconditioner_test.go +++ b/pkg/report/result/reconditioner_test.go @@ -3,10 +3,11 @@ package result_test import ( "testing" - "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" - "github.com/kyverno/policy-reporter/pkg/report/result" corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" + "github.com/kyverno/policy-reporter/pkg/report/result" ) func TestReconditioner(t *testing.T) { @@ -58,9 +59,6 @@ func TestReconditioner(t *testing.T) { if res.Category != "Other" { t.Error("result category should default to Other") } - if res.Priority != v1alpha2.ErrorPriority { - t.Error("result prioriry should be mapped") - } if len(res.Resources) == 0 || res.Resources[0] != *report.GetScope() { t.Error("result resource should be mapped to scope") } @@ -116,9 +114,6 @@ func TestReconditioner(t *testing.T) { if res.Category != "Other" { t.Error("result category should default to Other") } - if res.Priority != v1alpha2.ErrorPriority { - t.Error("result prioriry should be mapped") - } if len(res.Resources) == 0 || res.Resources[0] != *report.GetScope() { t.Error("result resource should be mapped to scope") } diff --git a/pkg/report/result/resource.go b/pkg/report/result/resource.go index cfa40b2a..c04cc095 100644 --- a/pkg/report/result/resource.go +++ b/pkg/report/result/resource.go @@ -1,8 +1,9 @@ package result import ( - "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" corev1 "k8s.io/api/core/v1" + + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" ) func Resource(p v1alpha2.ReportInterface, r v1alpha2.PolicyReportResult) *corev1.ObjectReference { diff --git a/pkg/report/result/resource_test.go b/pkg/report/result/resource_test.go index b1d9c2b0..1cd3c619 100644 --- a/pkg/report/result/resource_test.go +++ b/pkg/report/result/resource_test.go @@ -3,9 +3,10 @@ package result_test import ( "testing" + corev1 "k8s.io/api/core/v1" + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" "github.com/kyverno/policy-reporter/pkg/report/result" - corev1 "k8s.io/api/core/v1" ) func TestResource(t *testing.T) { diff --git a/pkg/report/result_filter.go b/pkg/report/result_filter.go new file mode 100644 index 00000000..70fe9178 --- /dev/null +++ b/pkg/report/result_filter.go @@ -0,0 +1,31 @@ +package report + +import ( + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" +) + +type ResultValidation = func(v1alpha2.PolicyReportResult) bool + +type ResultFilter struct { + validations []ResultValidation + Sources []string + MinimumSeverity string +} + +func (rf *ResultFilter) AddValidation(v ResultValidation) { + rf.validations = append(rf.validations, v) +} + +func (rf *ResultFilter) Validate(result v1alpha2.PolicyReportResult) bool { + for _, validation := range rf.validations { + if !validation(result) { + return false + } + } + + return true +} + +func NewResultFilter() *ResultFilter { + return &ResultFilter{} +} diff --git a/pkg/report/result_filter_test.go b/pkg/report/result_filter_test.go new file mode 100644 index 00000000..bfcc0b8a --- /dev/null +++ b/pkg/report/result_filter_test.go @@ -0,0 +1,25 @@ +package report_test + +import ( + "testing" + + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" + "github.com/kyverno/policy-reporter/pkg/fixtures" + "github.com/kyverno/policy-reporter/pkg/report" +) + +func Test_ResultFilter(t *testing.T) { + t.Run("don't filter any result without validations", func(t *testing.T) { + filter := report.NewResultFilter() + if !filter.Validate(fixtures.FailResult) { + t.Error("Expected result validates to true") + } + }) + t.Run("filter result with a false validation", func(t *testing.T) { + filter := report.NewResultFilter() + filter.AddValidation(func(r v1alpha2.PolicyReportResult) bool { return false }) + if filter.Validate(fixtures.FailResult) { + t.Error("Expected result validates to false") + } + }) +} diff --git a/pkg/report/source_filter.go b/pkg/report/source_filter.go new file mode 100644 index 00000000..2d40515c --- /dev/null +++ b/pkg/report/source_filter.go @@ -0,0 +1,154 @@ +package report + +import ( + "strings" + + "go.uber.org/zap" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" + "github.com/kyverno/policy-reporter/pkg/helper" + "github.com/kyverno/policy-reporter/pkg/validate" +) + +type PodClient interface { + Get(res *corev1.ObjectReference) (*corev1.Pod, error) +} + +type JobClient interface { + Get(res *corev1.ObjectReference) (*batchv1.Job, error) +} + +type ReportSelector struct { + Source string +} + +type SourceValidation struct { + Selector ReportSelector + Kinds validate.RuleSets + Sources validate.RuleSets + Namespaces validate.RuleSets + UncontrolledOnly bool + DisableClusterReports bool +} + +type SourceFilter struct { + pods PodClient + jobs JobClient + validations []SourceValidation +} + +func (s *SourceFilter) Validate(polr v1alpha2.ReportInterface) bool { + for _, validation := range s.validations { + if ok := s.run(polr, validation); !ok { + return false + } + } + + return true +} + +func (s *SourceFilter) run(polr v1alpha2.ReportInterface, options SourceValidation) bool { + logger := zap.L().With( + zap.String("namespace", polr.GetNamespace()), + zap.String("report", polr.GetName()), + ) + + if !Match(polr, options.Selector) { + return true + } + + if options.DisableClusterReports && polr.GetNamespace() == "" { + logger.Debug("filter cluster report") + return false + } + + if options.Sources.Enabled() && !validate.MatchRuleSet(polr.GetSource(), options.Sources) { + logger.Debug("filter report source") + return false + } + + scope := polr.GetScope() + if scope == nil { + return true + } + + logger = logger.With( + zap.String("kind", scope.Kind), + zap.String("name", scope.Name), + zap.String("namespace", scope.Namespace), + ) + + if options.Kinds.Enabled() && !validate.MatchRuleSet(scope.Kind, options.Kinds) { + logger.Debug("filter scope resource kind") + return false + } + + if options.Namespaces.Enabled() && !validate.MatchRuleSet(scope.Namespace, options.Namespaces) { + logger.Debug("filter scope resource namespace") + return false + } + + if options.UncontrolledOnly && s.pods != nil && scope.Kind == "Pod" { + pod, err := s.pods.Get(scope) + if err != nil { + logger.Error("failed to get pod", zap.Error(err), zap.String("name", scope.Name), zap.String("namespace", scope.Namespace)) + return true + } + + if ok := Uncontrolled(pod.OwnerReferences); ok { + return true + } + + logger.Debug("filter controlled pod resource") + return false + } + + if options.UncontrolledOnly && s.jobs != nil && scope.Kind == "Job" { + job, err := s.jobs.Get(scope) + if err != nil { + logger.Error("failed to get job", zap.Error(err)) + return true + } + + if ok := Uncontrolled(job.OwnerReferences); ok { + return true + } + + logger.Debug("filter controlled job resource") + return false + } + + return true +} + +func NewSourceFilter(pods PodClient, jobs JobClient, validations []SourceValidation) *SourceFilter { + return &SourceFilter{pods: pods, jobs: jobs, validations: validations} +} + +var controller = []string{"ReplicaSet", "DaemonSet", "CronJob", "Job", "Job", "StatefulSet"} + +func Uncontrolled(owner []metav1.OwnerReference) bool { + if len(owner) == 0 { + return true + } + + for _, o := range owner { + isController := o.Controller + if isController == nil { + continue + } + + if *isController == true && helper.Contains(o.Kind, controller) { + return false + } + } + + return true +} + +func Match(polr v1alpha2.ReportInterface, selector ReportSelector) bool { + return selector.Source == "" || strings.ToLower(selector.Source) == strings.ToLower(polr.GetSource()) +} diff --git a/pkg/target/client.go b/pkg/target/client.go index 7e96a97f..25d849ce 100644 --- a/pkg/target/client.go +++ b/pkg/target/client.go @@ -5,41 +5,54 @@ import ( "strings" "github.com/kyverno/go-wildcard" + "go.uber.org/zap" "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" - "github.com/kyverno/policy-reporter/pkg/helper" + "github.com/kyverno/policy-reporter/pkg/kubernetes/namespaces" "github.com/kyverno/policy-reporter/pkg/report" "github.com/kyverno/policy-reporter/pkg/validate" ) +type ClientType = string + +const ( + SingleSend ClientType = "single" + BatchSend ClientType = "batch" + SyncSend ClientType = "sync" +) + // Client for a provided Target type Client interface { // Send the given Result to the configured Target Send(result v1alpha2.PolicyReportResult) + // BatchSend the given Results of a single PolicyReport to the configured Target + BatchSend(report v1alpha2.ReportInterface, results []v1alpha2.PolicyReportResult) // SkipExistingOnStartup skips already existing PolicyReportResults on startup SkipExistingOnStartup() bool // Name is a unique identifier for each Target Name() string // Validate if a result should send Validate(rep v1alpha2.ReportInterface, result v1alpha2.PolicyReportResult) bool - // MinimumPriority for a triggered Result to send to this target - MinimumPriority() string + // MinimumSeverity for a triggered Result to send to this target + MinimumSeverity() string // Sources of the Results which should send to this target, empty means all sources Sources() []string - // Cleanup old results if supported by the target + // Type for the given target + Type() ClientType + // CleanUp old results if supported by the target CleanUp(context.Context, v1alpha2.ReportInterface) + // Reset the current state in the related target + Reset(context.Context) error } -func NewResultFilter(namespace, priority, policy validate.RuleSets, minimumPriority string, sources []string) *report.ResultFilter { - f := report.NewResultFilter() - f.Sources = sources - f.MinimumPriority = minimumPriority +type ResultFilterFactory struct { + client namespaces.Client +} - if len(sources) > 0 { - f.AddValidation(func(r v1alpha2.PolicyReportResult) bool { - return helper.Contains(r.Source, sources) - }) - } +func (rf *ResultFilterFactory) CreateFilter(namespace, severity, status, policy, sources validate.RuleSets, minimumSeverity string) *report.ResultFilter { + f := report.NewResultFilter() + f.Sources = sources.Include + f.MinimumSeverity = minimumSeverity if namespace.Count() > 0 { f.AddValidation(func(r v1alpha2.PolicyReportResult) bool { @@ -51,9 +64,31 @@ func NewResultFilter(namespace, priority, policy validate.RuleSets, minimumPrior }) } - if minimumPriority != "" { + if len(namespace.Selector) > 0 { f.AddValidation(func(r v1alpha2.PolicyReportResult) bool { - return r.Priority >= v1alpha2.NewPriority(f.MinimumPriority) + if r.GetResource() == nil || r.GetResource().Namespace == "" { + return true + } + + namespaces, err := rf.client.List(context.Background(), namespace.Selector) + if err != nil { + zap.L().Error("failed to resolve namespace selector", zap.Error(err)) + return false + } + + return validate.Namespace(r.GetResource().Namespace, validate.RuleSets{Include: namespaces}) + }) + } + + if minimumSeverity != "" { + f.AddValidation(func(r v1alpha2.PolicyReportResult) bool { + return v1alpha2.SeverityLevel[r.Severity] >= v1alpha2.SeverityLevel[v1alpha2.PolicySeverity(f.MinimumSeverity)] + }) + } + + if sources.Count() > 0 { + f.AddValidation(func(r v1alpha2.PolicyReportResult) bool { + return validate.MatchRuleSet(r.Source, sources) }) } @@ -63,17 +98,24 @@ func NewResultFilter(namespace, priority, policy validate.RuleSets, minimumPrior }) } - if priority.Count() > 0 { + if severity.Count() > 0 { f.AddValidation(func(r v1alpha2.PolicyReportResult) bool { - return validate.ContainsRuleSet(r.Priority.String(), priority) + return validate.ContainsRuleSet(string(r.Severity), severity) + }) + } + + if status.Count() > 0 { + f.AddValidation(func(r v1alpha2.PolicyReportResult) bool { + return validate.ContainsRuleSet(string(r.Result), status) }) } return f } -func NewReportFilter(labels validate.RuleSets) *report.ReportFilter { +func NewReportFilter(labels, sources validate.RuleSets) *report.ReportFilter { f := report.NewReportFilter() + if labels.Count() > 0 { f.AddValidation(func(r v1alpha2.ReportInterface) bool { if len(labels.Include) > 0 { @@ -116,9 +158,24 @@ func NewReportFilter(labels validate.RuleSets) *report.ReportFilter { }) } + if sources.Count() > 0 { + f.AddValidation(func(r v1alpha2.ReportInterface) bool { + source := r.GetSource() + if source == "" { + return true + } + + return validate.MatchRuleSet(source, sources) + }) + } + return f } +func NewResultFilterFactory(client namespaces.Client) *ResultFilterFactory { + return &ResultFilterFactory{client: client} +} + type BaseClient struct { name string skipExistingOnStartup bool @@ -137,12 +194,12 @@ func (c *BaseClient) Name() string { return c.name } -func (c *BaseClient) MinimumPriority() string { +func (c *BaseClient) MinimumSeverity() string { if c.resultFilter == nil { - return v1alpha2.DefaultPriority.String() + return v1alpha2.SeverityInfo } - return c.resultFilter.MinimumPriority + return c.resultFilter.MinimumSeverity } func (c *BaseClient) Sources() []string { @@ -154,11 +211,7 @@ func (c *BaseClient) Sources() []string { } func (c *BaseClient) Validate(rep v1alpha2.ReportInterface, result v1alpha2.PolicyReportResult) bool { - if rep == nil { - return false - } - - if c.reportFilter != nil && !c.reportFilter.Validate(rep) { + if !c.ValidateReport(rep) { return false } @@ -169,10 +222,30 @@ func (c *BaseClient) Validate(rep v1alpha2.ReportInterface, result v1alpha2.Poli return true } +func (c *BaseClient) ValidateReport(rep v1alpha2.ReportInterface) bool { + if rep == nil { + return false + } + + if c.reportFilter != nil && !c.reportFilter.Validate(rep) { + return false + } + + return true +} + func (c *BaseClient) SkipExistingOnStartup() bool { return c.skipExistingOnStartup } +func (c *BaseClient) Reset(_ context.Context) error { + return nil +} + +func (c *BaseClient) CleanUp(_ context.Context, _ v1alpha2.ReportInterface) {} + +func (c *BaseClient) BatchSend(_ v1alpha2.ReportInterface, _ []v1alpha2.PolicyReportResult) {} + func NewBaseClient(options ClientOptions) BaseClient { return BaseClient{options.Name, options.SkipExistingOnStartup, options.ResultFilter, options.ReportFilter} } diff --git a/pkg/target/client_test.go b/pkg/target/client_test.go index 90f64be7..8f90e5fe 100644 --- a/pkg/target/client_test.go +++ b/pkg/target/client_test.go @@ -3,6 +3,7 @@ package target_test import ( "testing" + "github.com/stretchr/testify/assert" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" @@ -18,187 +19,226 @@ var preport = &v1alpha2.PolicyReport{ }, } +var factory = target.NewResultFilterFactory(nil) + func Test_BaseClient(t *testing.T) { - t.Run("Validate MinimumPriority", func(t *testing.T) { - filter := target.NewResultFilter( + t.Run("Validate MinimumSeverity", func(t *testing.T) { + filter := factory.CreateFilter( validate.RuleSets{}, validate.RuleSets{}, validate.RuleSets{}, - "error", - make([]string, 0), + validate.RuleSets{}, + validate.RuleSets{}, + v1alpha2.SeverityCritical, ) - if filter.Validate(fixtures.FailResult) { - t.Errorf("Unexpected Validation Result") - } + assert.False(t, filter.Validate(fixtures.FailResult), "Unexpected Validation Result") }) t.Run("Validate Source", func(t *testing.T) { - filter := target.NewResultFilter( + filter := factory.CreateFilter( validate.RuleSets{}, validate.RuleSets{}, validate.RuleSets{}, + validate.RuleSets{}, + validate.RuleSets{Include: []string{"jsPolicy"}}, "", - []string{"jsPolicy"}, ) - if filter.Validate(fixtures.FailResult) { - t.Errorf("Unexpected Validation Result") - } + assert.False(t, filter.Validate(fixtures.FailResult), "Unexpected Validation Result") }) t.Run("Validate ClusterResult", func(t *testing.T) { - filter := target.NewResultFilter( + filter := factory.CreateFilter( validate.RuleSets{Include: []string{"default"}}, validate.RuleSets{}, validate.RuleSets{}, + validate.RuleSets{}, + validate.RuleSets{}, "", - make([]string, 0), ) - if !filter.Validate(fixtures.FailResultWithoutResource) { - t.Errorf("Unexpected Validation Result") - } + assert.True(t, filter.Validate(fixtures.FailResultWithoutResource), "Unexpected Validation Result") }) t.Run("Validate Exclude Namespace match", func(t *testing.T) { - filter := target.NewResultFilter( + filter := factory.CreateFilter( validate.RuleSets{Exclude: []string{"test"}}, validate.RuleSets{}, validate.RuleSets{}, + validate.RuleSets{}, + validate.RuleSets{}, "", - make([]string, 0), ) - if filter.Validate(fixtures.FailResult) { - t.Errorf("Unexpected Validation Result") - } + assert.False(t, filter.Validate(fixtures.FailResult), "Unexpected Validation Result") }) t.Run("Validate Exclude Namespace mismatch", func(t *testing.T) { - filter := target.NewResultFilter( + filter := factory.CreateFilter( validate.RuleSets{Exclude: []string{"team-a"}}, validate.RuleSets{}, validate.RuleSets{}, + validate.RuleSets{}, + validate.RuleSets{}, "", - make([]string, 0), ) - if !filter.Validate(fixtures.FailResult) { - t.Errorf("Unexpected Validation Result") - } + assert.True(t, filter.Validate(fixtures.FailResult), "Unexpected Validation Result") }) t.Run("Validate Include Namespace match", func(t *testing.T) { - filter := target.NewResultFilter( + filter := factory.CreateFilter( validate.RuleSets{Include: []string{"test"}}, validate.RuleSets{}, validate.RuleSets{}, + validate.RuleSets{}, + validate.RuleSets{}, "", - make([]string, 0), ) - if !filter.Validate(fixtures.FailResult) { - t.Errorf("Unexpected Validation Result") - } + assert.True(t, filter.Validate(fixtures.FailResult), "Unexpected Validation Result") }) t.Run("Validate Exclude Namespace mismatch", func(t *testing.T) { - filter := target.NewResultFilter( + filter := factory.CreateFilter( validate.RuleSets{Include: []string{"team-a"}}, validate.RuleSets{}, validate.RuleSets{}, + validate.RuleSets{}, + validate.RuleSets{}, "", - make([]string, 0), ) - if filter.Validate(fixtures.FailResult) { - t.Errorf("Unexpected Validation Result") - } + assert.False(t, filter.Validate(fixtures.FailResult), "Unexpected Validation Result") }) - t.Run("Validate Exclude Priority match", func(t *testing.T) { - filter := target.NewResultFilter( + t.Run("Validate Exclude Status match", func(t *testing.T) { + filter := factory.CreateFilter( + validate.RuleSets{}, + validate.RuleSets{}, + validate.RuleSets{Exclude: []string{v1alpha2.StatusFail}}, validate.RuleSets{}, - validate.RuleSets{Exclude: []string{v1alpha2.WarningPriority.String()}}, validate.RuleSets{}, "", - make([]string, 0), ) - if filter.Validate(fixtures.FailResult) { - t.Errorf("Unexpected Validation Result") - } + assert.False(t, filter.Validate(fixtures.FailResult), "Unexpected Validation Result") }) - t.Run("Validate Exclude Priority mismatch", func(t *testing.T) { - filter := target.NewResultFilter( + t.Run("Validate Exclude Status mismatch", func(t *testing.T) { + filter := factory.CreateFilter( + validate.RuleSets{}, + validate.RuleSets{}, + validate.RuleSets{Exclude: []string{v1alpha2.StatusSkip}}, validate.RuleSets{}, - validate.RuleSets{Exclude: []string{v1alpha2.ErrorPriority.String()}}, validate.RuleSets{}, "", - make([]string, 0), ) - if !filter.Validate(fixtures.FailResult) { - t.Errorf("Unexpected Validation Result") - } + assert.True(t, filter.Validate(fixtures.FailResult), "Unexpected Validation Result") }) - t.Run("Validate Include Priority match", func(t *testing.T) { - filter := target.NewResultFilter( + t.Run("Validate Include Status match", func(t *testing.T) { + filter := factory.CreateFilter( + validate.RuleSets{}, + validate.RuleSets{}, + validate.RuleSets{Include: []string{v1alpha2.StatusFail}}, validate.RuleSets{}, - validate.RuleSets{Include: []string{v1alpha2.WarningPriority.String()}}, validate.RuleSets{}, "", - make([]string, 0), ) - if !filter.Validate(fixtures.FailResult) { - t.Errorf("Unexpected Validation Result") - } + assert.True(t, filter.Validate(fixtures.FailResult), "Unexpected Validation Result") }) - t.Run("Validate Exclude Priority mismatch", func(t *testing.T) { - filter := target.NewResultFilter( + t.Run("Validate Exclude Status mismatch", func(t *testing.T) { + filter := factory.CreateFilter( + validate.RuleSets{}, + validate.RuleSets{}, + validate.RuleSets{Exclude: []string{v1alpha2.StatusFail}}, validate.RuleSets{}, - validate.RuleSets{Include: []string{v1alpha2.ErrorPriority.String()}}, validate.RuleSets{}, "", - make([]string, 0), ) - if filter.Validate(fixtures.FailResult) { - t.Errorf("Unexpected Validation Result") - } + assert.False(t, filter.Validate(fixtures.FailResult), "Unexpected Validation Result") + }) + + t.Run("Validate Exclude Severity match", func(t *testing.T) { + filter := factory.CreateFilter( + validate.RuleSets{}, + validate.RuleSets{Exclude: []string{v1alpha2.SeverityHigh}}, + validate.RuleSets{}, + validate.RuleSets{}, + validate.RuleSets{}, + "", + ) + + assert.False(t, filter.Validate(fixtures.FailResult), "Unexpected Validation Result") + }) + t.Run("Validate Exclude Severity mismatch", func(t *testing.T) { + filter := factory.CreateFilter( + validate.RuleSets{}, + validate.RuleSets{Exclude: []string{v1alpha2.SeverityCritical}}, + validate.RuleSets{}, + validate.RuleSets{}, + validate.RuleSets{}, + "", + ) + + assert.True(t, filter.Validate(fixtures.FailResult), "Unexpected Validation Result") + }) + t.Run("Validate Include Severity match", func(t *testing.T) { + filter := factory.CreateFilter( + validate.RuleSets{}, + validate.RuleSets{Include: []string{v1alpha2.SeverityHigh}}, + validate.RuleSets{}, + validate.RuleSets{}, + validate.RuleSets{}, + "", + ) + + assert.True(t, filter.Validate(fixtures.FailResult), "Unexpected Validation Result") + }) + t.Run("Validate Exclude Severity mismatch", func(t *testing.T) { + filter := factory.CreateFilter( + validate.RuleSets{}, + validate.RuleSets{Include: []string{v1alpha2.SeverityCritical}}, + validate.RuleSets{}, + validate.RuleSets{}, + validate.RuleSets{}, + "", + ) + + assert.False(t, filter.Validate(fixtures.FailResult), "Unexpected Validation Result") }) t.Run("Validate Exclude Policy match", func(t *testing.T) { - filter := target.NewResultFilter( + filter := factory.CreateFilter( + validate.RuleSets{}, validate.RuleSets{}, validate.RuleSets{}, validate.RuleSets{Exclude: []string{"require-requests-and-limits-required"}}, + validate.RuleSets{}, "", - make([]string, 0), ) - if filter.Validate(fixtures.FailResult) { - t.Errorf("Unexpected Validation Result") - } + assert.False(t, filter.Validate(fixtures.FailResult), "Unexpected Validation Result") }) t.Run("Validate Exclude Policy mismatch", func(t *testing.T) { - filter := target.NewResultFilter( + filter := factory.CreateFilter( + validate.RuleSets{}, validate.RuleSets{}, validate.RuleSets{}, validate.RuleSets{Exclude: []string{"policy-test"}}, + validate.RuleSets{}, "", - make([]string, 0), ) - if !filter.Validate(fixtures.FailResult) { - t.Errorf("Unexpected Validation Result") - } + assert.True(t, filter.Validate(fixtures.FailResult), "Unexpected Validation Result") }) t.Run("Validate Include Policy match", func(t *testing.T) { - filter := target.NewResultFilter( + filter := factory.CreateFilter( + validate.RuleSets{}, validate.RuleSets{}, validate.RuleSets{}, validate.RuleSets{Include: []string{"require-requests-and-limits-required"}}, + validate.RuleSets{}, "", - make([]string, 0), ) if !filter.Validate(fixtures.FailResult) { @@ -206,31 +246,30 @@ func Test_BaseClient(t *testing.T) { } }) t.Run("Validate Exclude Policy mismatch", func(t *testing.T) { - filter := target.NewResultFilter( + filter := factory.CreateFilter( + validate.RuleSets{}, validate.RuleSets{}, validate.RuleSets{}, validate.RuleSets{Include: []string{"policy-test"}}, + validate.RuleSets{}, "", - make([]string, 0), ) - if filter.Validate(fixtures.FailResult) { - t.Errorf("Unexpected Validation Result") - } + assert.False(t, filter.Validate(fixtures.FailResult), "Unexpected Validation Result") }) t.Run("Validate Include Label match", func(t *testing.T) { filter := target.NewReportFilter( validate.RuleSets{Include: []string{"app:policy-reporter"}}, + validate.RuleSets{}, ) - if !filter.Validate(preport) { - t.Errorf("Unexpected Validation Result") - } + assert.True(t, filter.Validate(preport), "Unexpected Validation Result") }) t.Run("Validate Exclude Label match", func(t *testing.T) { filter := target.NewReportFilter( validate.RuleSets{Exclude: []string{"app:policy-reporter"}}, + validate.RuleSets{}, ) if filter.Validate(preport) { @@ -240,97 +279,92 @@ func Test_BaseClient(t *testing.T) { t.Run("Validate Exclude Label mismatch", func(t *testing.T) { filter := target.NewReportFilter( validate.RuleSets{Exclude: []string{"app:monitoring"}}, + validate.RuleSets{}, ) - if !filter.Validate(preport) { - t.Errorf("Unexpected Validation Result") - } + assert.True(t, filter.Validate(preport), "Unexpected Validation Result") }) t.Run("Validate Include Label mismatch", func(t *testing.T) { filter := target.NewReportFilter( validate.RuleSets{Include: []string{"app:monitoring"}}, + validate.RuleSets{}, ) - if filter.Validate(preport) { - t.Errorf("Unexpected Validation Result") - } + assert.False(t, filter.Validate(preport), "Unexpected Validation Result") }) t.Run("Validate label as wildcard filter", func(t *testing.T) { filter := target.NewReportFilter( validate.RuleSets{Exclude: []string{"app"}}, + validate.RuleSets{}, ) - if filter.Validate(preport) { - t.Errorf("Unexpected Validation Result") - } + assert.False(t, filter.Validate(preport), "Unexpected Validation Result") filter = target.NewReportFilter( validate.RuleSets{Include: []string{"app"}}, + validate.RuleSets{}, ) - if !filter.Validate(preport) { - t.Errorf("Unexpected Validation Result") - } + assert.True(t, filter.Validate(preport), "Unexpected Validation Result") }) t.Run("Validate Include Label wildcard", func(t *testing.T) { filter := target.NewReportFilter( validate.RuleSets{Include: []string{"app:*"}}, + validate.RuleSets{}, ) - if !filter.Validate(preport) { - t.Errorf("Unexpected Validation Result") - } + assert.True(t, filter.Validate(preport), "Unexpected Validation Result") }) t.Run("Validate Exclude Label wildcard", func(t *testing.T) { filter := target.NewReportFilter( validate.RuleSets{Exclude: []string{"app:*"}}, + validate.RuleSets{}, ) - if filter.Validate(preport) { - t.Errorf("Unexpected Validation Result") - } + assert.False(t, filter.Validate(preport), "Unexpected Validation Result") }) t.Run("Client Result Validation", func(t *testing.T) { client := target.NewBaseClient(target.ClientOptions{ Name: "Client", - ResultFilter: target.NewResultFilter( + ResultFilter: factory.CreateFilter( validate.RuleSets{}, validate.RuleSets{}, validate.RuleSets{Include: []string{"policy-test"}}, + validate.RuleSets{}, + validate.RuleSets{Include: []string{"jsPolicy"}}, "", - []string{"jsPolicy"}, ), SkipExistingOnStartup: true, }) - if client.Validate(&v1alpha2.PolicyReport{}, fixtures.FailResult) { - t.Errorf("Unexpected Validation Result") - } + assert.False(t, client.Validate(&v1alpha2.PolicyReport{}, fixtures.FailResult), "Unexpected Validation Result") }) t.Run("Client Report Validation", func(t *testing.T) { client := target.NewBaseClient(target.ClientOptions{ - Name: "Client", - ReportFilter: target.NewReportFilter(validate.RuleSets{Include: []string{"app"}}), + Name: "Client", + ReportFilter: target.NewReportFilter( + validate.RuleSets{Include: []string{"app"}}, + validate.RuleSets{}, + ), SkipExistingOnStartup: true, }) - if client.Validate(&v1alpha2.PolicyReport{}, fixtures.FailResult) { - t.Errorf("Unexpected Validation Result") - } + assert.False(t, client.Validate(&v1alpha2.PolicyReport{}, fixtures.FailResult), "Unexpected Validation Result") }) t.Run("Client nil Validation", func(t *testing.T) { client := target.NewBaseClient(target.ClientOptions{ - Name: "Client", - ReportFilter: target.NewReportFilter(validate.RuleSets{Include: []string{"app"}}), + Name: "Client", + ReportFilter: target.NewReportFilter( + validate.RuleSets{Include: []string{"app"}}, + validate.RuleSets{}, + ), SkipExistingOnStartup: true, }) - if client.Validate(nil, fixtures.FailResult) { - t.Errorf("Unexpected Validation Result") - } + assert.False(t, client.Validate(nil, fixtures.FailResult), "Unexpected Validation Result") }) t.Run("Client Validation Fallbacks", func(t *testing.T) { @@ -339,15 +373,9 @@ func Test_BaseClient(t *testing.T) { SkipExistingOnStartup: true, }) - if !client.Validate(&v1alpha2.PolicyReport{}, fixtures.FailResult) { - t.Errorf("Should fallback to true") - } - if client.MinimumPriority() != v1alpha2.DefaultPriority.String() { - t.Errorf("Should fallback to default priority") - } - if len(client.Sources()) != 0 || client.Sources() == nil { - t.Errorf("Should fallback to empty list") - } + assert.True(t, client.Validate(&v1alpha2.PolicyReport{}, fixtures.FailResult), "Should fallback to true") + assert.Equal(t, client.MinimumSeverity(), v1alpha2.SeverityInfo, "Should fallback to severity info") + assert.NotNil(t, client.Sources(), "Should fallback to empty list") }) t.Run("SkipExistingOnStartup", func(t *testing.T) { @@ -357,31 +385,25 @@ func Test_BaseClient(t *testing.T) { SkipExistingOnStartup: true, }) - if !client.SkipExistingOnStartup() { - t.Error("Should return configured SkipExistingOnStartup") - } + assert.True(t, client.SkipExistingOnStartup(), "Should return configured SkipExistingOnStartup") }) - t.Run("MinimumPriority", func(t *testing.T) { + t.Run("MinimumSeverity", func(t *testing.T) { client := target.NewBaseClient(target.ClientOptions{ Name: "Client", - ResultFilter: &report.ResultFilter{MinimumPriority: "error"}, + ResultFilter: &report.ResultFilter{MinimumSeverity: v1alpha2.SeverityHigh}, SkipExistingOnStartup: true, }) - if client.MinimumPriority() != "error" { - t.Error("Should return configured MinimumPriority") - } + assert.Equal(t, client.MinimumSeverity(), v1alpha2.SeverityHigh, "Should return configured MinimumSeverity") }) t.Run("Name", func(t *testing.T) { client := target.NewBaseClient(target.ClientOptions{ Name: "Client", - ResultFilter: &report.ResultFilter{MinimumPriority: "error"}, + ResultFilter: &report.ResultFilter{MinimumSeverity: "error"}, SkipExistingOnStartup: true, }) - if client.Name() != "Client" { - t.Error("Should return configured Name") - } + assert.Equal(t, client.Name(), "Client", "Should return configured Name") }) t.Run("Sources", func(t *testing.T) { client := target.NewBaseClient(target.ClientOptions{ @@ -390,11 +412,7 @@ func Test_BaseClient(t *testing.T) { SkipExistingOnStartup: true, }) - if len(client.Sources()) != 1 { - t.Fatal("Unexpected length of Sources") - } - if client.Sources()[0] != "Kyverno" { - t.Error("Unexptected Source returned") - } + assert.Len(t, client.Sources(), 1) + assert.Equal(t, client.Sources()[0], "Kyverno") }) } diff --git a/pkg/target/collection.go b/pkg/target/collection.go new file mode 100644 index 00000000..090ba33a --- /dev/null +++ b/pkg/target/collection.go @@ -0,0 +1,144 @@ +package target + +import ( + "context" + "sync" + + "go.uber.org/zap" + + "github.com/kyverno/policy-reporter/pkg/helper" +) + +type TargetType = string + +const ( + Loki TargetType = "Loki" + Elasticsearch TargetType = "Elasticsearch" + Slack TargetType = "Slack" + Discord TargetType = "Discord" + Teams TargetType = "Teams" + GoogleChat TargetType = "GoogleChat" + Telegram TargetType = "Telegram" + Webhook TargetType = "Webhook" + S3 TargetType = "S3" + Kinesis TargetType = "Kinesis" + SecurityHub TargetType = "SecurityHub" + GCS TargetType = "GCS" +) + +type TargetConfig interface { + Secret() string +} + +type Target struct { + ID string + Type TargetType + Client Client + ParentConfig TargetConfig + Config TargetConfig +} + +func (t *Target) Secret() string { + if t.Config.Secret() != "" { + return t.Config.Secret() + } + + return t.ParentConfig.Secret() +} + +type Collection struct { + mx *sync.Mutex + clients []Client + targets map[string]*Target +} + +func (c *Collection) Update(t *Target) { + c.mx.Lock() + c.targets[t.ID] = t + c.clients = make([]Client, 0) + c.mx.Unlock() +} + +func (c *Collection) Reset(ctx context.Context) bool { + clients := c.SyncClients() + + for _, c := range clients { + if err := c.Reset(ctx); err != nil { + zap.L().Error("failed to reset target", zap.String("type", c.Type()), zap.String("name", c.Name())) + } + } + + return true +} + +func (c *Collection) Targets() []*Target { + return helper.ToList(c.targets) +} + +func (c *Collection) Clients() []Client { + if len(c.clients) != 0 { + return c.clients + } + + c.clients = helper.MapSlice(c.targets, func(t *Target) Client { + return t.Client + }) + + return c.clients +} + +func (c *Collection) Client(name string) Client { + return helper.Find(c.Clients(), func(c Client) bool { + return c.Name() == name + }, nil) +} + +func (c *Collection) SingleSendClients() []Client { + return helper.Filter(c.Clients(), func(c Client) bool { + return c.Type() == SingleSend + }) +} + +func (c *Collection) SyncClients() []Client { + return helper.Filter(c.Clients(), func(c Client) bool { + return c.Type() == SyncSend + }) +} + +func (c *Collection) BatchSendClients() []Client { + return helper.Filter(c.Clients(), func(c Client) bool { + return c.Type() == BatchSend + }) +} + +func (c *Collection) UsesSecrets() bool { + useSecrets := helper.Filter(c.Targets(), func(t *Target) bool { + return t.Secret() != "" + }) + + return len(useSecrets) > 0 +} + +func (c *Collection) Empty() bool { + return c.Length() == 0 +} + +func (c *Collection) Length() int { + return len(c.targets) +} + +func NewCollection(targets ...*Target) *Collection { + collection := &Collection{ + clients: make([]Client, 0), + targets: make(map[string]*Target, 0), + mx: new(sync.Mutex), + } + + for _, t := range targets { + if t != nil { + collection.Update(t) + } + } + + return collection +} diff --git a/pkg/target/collection_test.go b/pkg/target/collection_test.go new file mode 100644 index 00000000..61b2e5e0 --- /dev/null +++ b/pkg/target/collection_test.go @@ -0,0 +1,88 @@ +package target_test + +import ( + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + + "github.com/kyverno/policy-reporter/pkg/target" + "github.com/kyverno/policy-reporter/pkg/target/discord" + "github.com/kyverno/policy-reporter/pkg/target/slack" + "github.com/kyverno/policy-reporter/pkg/target/webhook" +) + +func TestCollection(t *testing.T) { + collection := target.NewCollection( + &target.Target{ + ID: uuid.NewString(), + Type: target.Webhook, + Client: webhook.NewClient(webhook.Options{ + ClientOptions: target.ClientOptions{ + Name: "Webhook", + }, + }), + Config: &target.Config[target.WebhookOptions]{SecretRef: "webhook-secret"}, + ParentConfig: &target.Config[target.WebhookOptions]{}, + }, + &target.Target{ + ID: uuid.NewString(), + Type: target.Slack, + Client: slack.NewClient(slack.Options{ + ClientOptions: target.ClientOptions{ + Name: "Slack", + }, + }), + Config: &target.Config[target.SlackOptions]{}, + ParentConfig: &target.Config[target.SlackOptions]{SecretRef: "slack-secret"}, + }, + &target.Target{ + ID: uuid.NewString(), + Type: target.Discord, + Client: discord.NewClient(discord.Options{ + ClientOptions: target.ClientOptions{ + Name: "Discord", + }, + }), + Config: &target.Config[target.WebhookOptions]{}, + ParentConfig: &target.Config[target.WebhookOptions]{SecretRef: "slack-secret"}, + }, + ) + + t.Run("empty returns if the collection has any target", func(t *testing.T) { + assert.True(t, target.NewCollection().Empty()) + assert.False(t, collection.Empty()) + }) + + t.Run("length returns the amount of targets within a collection", func(t *testing.T) { + assert.Equal(t, collection.Length(), 3) + }) + + t.Run("clients returns all clients of the given targets", func(t *testing.T) { + assert.Equal(t, len(collection.Clients()), 3) + }) + + t.Run("client searches for a configured target with the given name", func(t *testing.T) { + assert.NotNil(t, collection.Client("Webhook")) + assert.NotNil(t, collection.Client("Discord")) + assert.NotNil(t, collection.Client("Slack")) + assert.Nil(t, collection.Client("Invalid")) + }) + + t.Run("usesSecret checks if at least on target has a secretRef configured", func(t *testing.T) { + assert.False(t, target.NewCollection().UsesSecrets()) + assert.True(t, collection.UsesSecrets()) + }) + + t.Run("SingleSendClients only returns clients which do not support batch sending", func(t *testing.T) { + for _, c := range collection.SingleSendClients() { + assert.Equal(t, target.SingleSend, c.Type()) + } + }) + + t.Run("BatchSendClients only returns clients which do support batch sending", func(t *testing.T) { + for _, c := range collection.BatchSendClients() { + assert.Equal(t, target.BatchSend, c.Type()) + } + }) +} diff --git a/pkg/target/discord/discord.go b/pkg/target/discord/discord.go index 05c653fc..58bb7f36 100644 --- a/pkg/target/discord/discord.go +++ b/pkg/target/discord/discord.go @@ -35,16 +35,16 @@ type embedField struct { Inline bool `json:"inline"` } -var colors = map[v1alpha2.Priority]string{ - v1alpha2.DebugPriority: "12370112", - v1alpha2.InfoPriority: "3066993", - v1alpha2.WarningPriority: "15105570", - v1alpha2.CriticalPriority: "15158332", - v1alpha2.ErrorPriority: "15158332", +var colors = map[v1alpha2.PolicySeverity]string{ + v1alpha2.SeverityInfo: "12370112", + v1alpha2.SeverityLow: "3066993", + v1alpha2.StatusWarn: "15105570", + v1alpha2.SeverityHigh: "15158332", + v1alpha2.SeverityCritical: "15158332", } func newPayload(result v1alpha2.PolicyReportResult, customFields map[string]string) payload { - color := colors[result.Priority] + color := colors[result.Severity] embedFields := make([]embedField, 0) @@ -54,8 +54,6 @@ func newPayload(result v1alpha2.PolicyReportResult, customFields map[string]stri embedFields = append(embedFields, embedField{"Rule", result.Rule, true}) } - embedFields = append(embedFields, embedField{"Priority", result.Priority.String(), true}) - if result.Category != "" { embedFields = append(embedFields, embedField{"Category", result.Category, true}) } @@ -106,7 +104,7 @@ type client struct { } func (d *client) Send(result v1alpha2.PolicyReportResult) { - req, err := http.CreateJSONRequest(d.Name(), "POST", d.webhook, newPayload(result, d.customFields)) + req, err := http.CreateJSONRequest("POST", d.webhook, newPayload(result, d.customFields)) if err != nil { return } @@ -117,6 +115,12 @@ func (d *client) Send(result v1alpha2.PolicyReportResult) { func (d *client) CleanUp(_ context.Context, _ v1alpha2.ReportInterface) {} +func (d *client) BatchSend(_ v1alpha2.ReportInterface, _ []v1alpha2.PolicyReportResult) {} + +func (d *client) Type() target.ClientType { + return target.SingleSend +} + // NewClient creates a new loki.client to send Results to Discord func NewClient(options Options) target.Client { return &client{ diff --git a/pkg/target/elasticsearch/elasticsearch.go b/pkg/target/elasticsearch/elasticsearch.go index 01760da6..211f6626 100644 --- a/pkg/target/elasticsearch/elasticsearch.go +++ b/pkg/target/elasticsearch/elasticsearch.go @@ -1,7 +1,6 @@ package elasticsearch import ( - "context" "time" "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" @@ -83,7 +82,7 @@ func (e *client) Send(result v1alpha2.PolicyReportResult) { result.Properties = props } - req, err := http.CreateJSONRequest(e.Name(), "POST", host, http.NewJSONResult(result)) + req, err := http.CreateJSONRequest("POST", host, http.NewJSONResult(result)) if err != nil { return } @@ -98,7 +97,9 @@ func (e *client) Send(result v1alpha2.PolicyReportResult) { http.ProcessHTTPResponse(e.Name(), resp, err) } -func (e *client) CleanUp(_ context.Context, _ v1alpha2.ReportInterface) {} +func (e *client) Type() target.ClientType { + return target.SingleSend +} // NewClient creates a new elasticsearch.client to send Results to Elasticsearch func NewClient(options Options) target.Client { diff --git a/pkg/target/factory.go b/pkg/target/factory.go new file mode 100644 index 00000000..9f50ab6c --- /dev/null +++ b/pkg/target/factory.go @@ -0,0 +1,172 @@ +package target + +type ValueFilter struct { + Include []string `mapstructure:"include"` + Exclude []string `mapstructure:"exclude"` + Selector map[string]any `mapstructure:"selector"` +} + +type Filter struct { + Namespaces ValueFilter `mapstructure:"namespaces"` + Status ValueFilter `mapstructure:"status"` + Severities ValueFilter `mapstructure:"severities"` + Policies ValueFilter `mapstructure:"policies"` + Sources ValueFilter `mapstructure:"sources"` + ReportLabels ValueFilter `mapstructure:"reportLabels"` +} + +type Config[T any] struct { + Config *T `mapstructure:"config"` + Name string `mapstructure:"name"` + MinimumSeverity string `mapstructure:"minimumSeverity"` + Filter Filter `mapstructure:"filter"` + SecretRef string `mapstructure:"secretRef"` + MountedSecret string `mapstructure:"mountedSecret"` + Sources []string `mapstructure:"sources"` + CustomFields map[string]string `mapstructure:"customFields"` + SkipExisting bool `mapstructure:"skipExistingOnStartup"` + Channels []*Config[T] `mapstructure:"channels"` + Valid bool `mapstructure:"-"` +} + +func (config *Config[T]) MapBaseParent(parent *Config[T]) { + if config.MinimumSeverity == "" { + config.MinimumSeverity = parent.MinimumSeverity + } + + if !config.SkipExisting { + config.SkipExisting = parent.SkipExisting + } +} + +func (config *Config[T]) Secret() string { + return config.SecretRef +} + +type AWSConfig struct { + AccessKeyID string `mapstructure:"accessKeyId"` + SecretAccessKey string `mapstructure:"secretAccessKey"` + Region string `mapstructure:"region"` + Endpoint string `mapstructure:"endpoint"` +} + +func (config *AWSConfig) MapAWSParent(parent AWSConfig) { + if config.Endpoint == "" { + config.Endpoint = parent.Endpoint + } + + if config.AccessKeyID == "" { + config.AccessKeyID = parent.AccessKeyID + } + + if config.SecretAccessKey == "" { + config.SecretAccessKey = parent.SecretAccessKey + } + + if config.Region == "" { + config.Region = parent.Region + } +} + +type WebhookOptions struct { + Webhook string `mapstructure:"webhook"` + SkipTLS bool `mapstructure:"skipTLS"` + Certificate string `mapstructure:"certificate"` + Headers map[string]string `mapstructure:"headers"` +} + +type HostOptions struct { + Host string `mapstructure:"host"` + SkipTLS bool `mapstructure:"skipTLS"` + Certificate string `mapstructure:"certificate"` + Headers map[string]string `mapstructure:"headers"` +} + +type TelegramOptions struct { + WebhookOptions `mapstructure:",squash"` + Token string `mapstructure:"token"` + ChatID string `mapstructure:"chatId"` +} + +type SlackOptions struct { + WebhookOptions `mapstructure:",squash"` + Channel string `mapstructure:"channel"` +} + +type LokiOptions struct { + HostOptions `mapstructure:",squash"` + Username string `mapstructure:"username"` + Password string `mapstructure:"password"` + Path string `mapstructure:"path"` +} + +type ElasticsearchOptions struct { + HostOptions `mapstructure:",squash"` + Index string `mapstructure:"index"` + Rotation string `mapstructure:"rotation"` + Username string `mapstructure:"username"` + Password string `mapstructure:"password"` + APIKey string `mapstructure:"apiKey"` + TypelessAPI bool `mapstructure:"typelessApi"` +} + +type S3Options struct { + AWSConfig `mapstructure:",squash"` + Prefix string `mapstructure:"prefix"` + Bucket string `mapstructure:"bucket"` + BucketKeyEnabled bool `mapstructure:"bucketKeyEnabled"` + KmsKeyID string `mapstructure:"kmsKeyId"` + ServerSideEncryption string `mapstructure:"serverSideEncryption"` + PathStyle bool `mapstructure:"pathStyle"` +} + +type KinesisOptions struct { + AWSConfig `mapstructure:",squash"` + StreamName string `mapstructure:"streamName"` +} + +type SecurityHubOptions struct { + AWSConfig `mapstructure:",squash"` + AccountID string `mapstructure:"accountId"` + ProductName string `mapstructure:"productName"` + CompanyName string `mapstructure:"companyName"` + DelayInSeconds int `mapstructure:"delayInSeconds"` + Synchronize bool `mapstructure:"synchronize"` +} + +type GCSOptions struct { + Credentials string `mapstructure:"credentials"` + Prefix string `mapstructure:"prefix"` + Bucket string `mapstructure:"bucket"` +} + +type Targets struct { + Loki *Config[LokiOptions] `mapstructure:"loki"` + Elasticsearch *Config[ElasticsearchOptions] `mapstructure:"elasticsearch"` + Slack *Config[SlackOptions] `mapstructure:"slack"` + Discord *Config[WebhookOptions] `mapstructure:"discord"` + Teams *Config[WebhookOptions] `mapstructure:"teams"` + Webhook *Config[WebhookOptions] `mapstructure:"webhook"` + GoogleChat *Config[WebhookOptions] `mapstructure:"googleChat"` + Telegram *Config[TelegramOptions] `mapstructure:"telegram"` + S3 *Config[S3Options] `mapstructure:"s3"` + Kinesis *Config[KinesisOptions] `mapstructure:"kinesis"` + SecurityHub *Config[SecurityHubOptions] `mapstructure:"securityHub"` + GCS *Config[GCSOptions] `mapstructure:"gcs"` +} + +type Factory interface { + CreateClients(config *Targets) *Collection + CreateLokiTarget(config, parent *Config[LokiOptions]) *Target + CreateElasticsearchTarget(config, parent *Config[ElasticsearchOptions]) *Target + CreateSlackTarget(config, parent *Config[SlackOptions]) *Target + CreateDiscordTarget(config, parent *Config[WebhookOptions]) *Target + CreateTeamsTarget(config, parent *Config[WebhookOptions]) *Target + CreateWebhookTarget(config, parent *Config[WebhookOptions]) *Target + CreateTelegramTarget(config, parent *Config[TelegramOptions]) *Target + CreateGoogleChatTarget(config, parent *Config[WebhookOptions]) *Target + CreateS3Target(config, parent *Config[S3Options]) *Target + CreateKinesisTarget(config, parent *Config[KinesisOptions]) *Target + CreateSecurityHubTarget(config, parent *Config[SecurityHubOptions]) *Target + CreateGCSTarget(config, parent *Config[GCSOptions]) *Target +} diff --git a/pkg/target/factory/factory.go b/pkg/target/factory/factory.go new file mode 100644 index 00000000..bb27bc4e --- /dev/null +++ b/pkg/target/factory/factory.go @@ -0,0 +1,960 @@ +package factory + +import ( + "context" + "encoding/json" + "fmt" + "os" + "strings" + "time" + + "github.com/google/uuid" + _ "github.com/mattn/go-sqlite3" + "go.uber.org/zap" + + "github.com/kyverno/policy-reporter/pkg/helper" + "github.com/kyverno/policy-reporter/pkg/kubernetes/secrets" + "github.com/kyverno/policy-reporter/pkg/report" + "github.com/kyverno/policy-reporter/pkg/target" + "github.com/kyverno/policy-reporter/pkg/target/discord" + "github.com/kyverno/policy-reporter/pkg/target/elasticsearch" + "github.com/kyverno/policy-reporter/pkg/target/gcs" + "github.com/kyverno/policy-reporter/pkg/target/googlechat" + "github.com/kyverno/policy-reporter/pkg/target/http" + "github.com/kyverno/policy-reporter/pkg/target/kinesis" + "github.com/kyverno/policy-reporter/pkg/target/loki" + "github.com/kyverno/policy-reporter/pkg/target/provider/aws" + gs "github.com/kyverno/policy-reporter/pkg/target/provider/gcs" + "github.com/kyverno/policy-reporter/pkg/target/s3" + "github.com/kyverno/policy-reporter/pkg/target/securityhub" + "github.com/kyverno/policy-reporter/pkg/target/slack" + "github.com/kyverno/policy-reporter/pkg/target/teams" + "github.com/kyverno/policy-reporter/pkg/target/telegram" + "github.com/kyverno/policy-reporter/pkg/target/webhook" + "github.com/kyverno/policy-reporter/pkg/validate" +) + +// TargetFactory manages target creation +type TargetFactory struct { + secretClient secrets.Client + filterFactory *target.ResultFilterFactory +} + +// LokiClients resolver method +func createClients[T any](name string, config *target.Config[T], mapper func(*target.Config[T], *target.Config[T]) *target.Target) []*target.Target { + clients := make([]*target.Target, 0) + if config == nil { + return clients + } + + if config.Config == nil { + config.Config = new(T) + } + + setFallback(&config.Name, name) + + if client := mapper(config, &target.Config[T]{Config: new(T)}); client != nil { + clients = append(clients, client) + config.Valid = true + } + + for i, channel := range config.Channels { + setFallback(&config.Name, fmt.Sprintf("%s Channel %d", name, i+1)) + + if channel.Config == nil { + channel.Config = new(T) + } + + if client := mapper(channel, config); client != nil { + clients = append(clients, client) + channel.Valid = true + } + } + + return clients +} + +// LokiClients resolver method +func (f *TargetFactory) CreateClients(config *target.Targets) *target.Collection { + targets := make([]*target.Target, 0) + if config == nil { + return target.NewCollection() + } + + targets = append(targets, createClients("Loki", config.Loki, f.CreateLokiTarget)...) + targets = append(targets, createClients("Elasticsearch", config.Elasticsearch, f.CreateElasticsearchTarget)...) + targets = append(targets, createClients("Slack", config.Slack, f.CreateSlackTarget)...) + targets = append(targets, createClients("Discord", config.Discord, f.CreateDiscordTarget)...) + targets = append(targets, createClients("Teams", config.Teams, f.CreateTeamsTarget)...) + targets = append(targets, createClients("GoogleChat", config.GoogleChat, f.CreateGoogleChatTarget)...) + targets = append(targets, createClients("Telegram", config.Telegram, f.CreateTelegramTarget)...) + targets = append(targets, createClients("Webhook", config.Webhook, f.CreateWebhookTarget)...) + targets = append(targets, createClients("S3", config.S3, f.CreateS3Target)...) + targets = append(targets, createClients("Kinesis", config.Kinesis, f.CreateKinesisTarget)...) + targets = append(targets, createClients("SecurityHub", config.SecurityHub, f.CreateSecurityHubTarget)...) + targets = append(targets, createClients("GoogleCloudStorage", config.GCS, f.CreateGCSTarget)...) + + return target.NewCollection(targets...) +} + +func (f *TargetFactory) CreateSlackTarget(config, parent *target.Config[target.SlackOptions]) *target.Target { + if config == nil { + return nil + } + + if (parent.SecretRef != "" && f.secretClient != nil) || parent.MountedSecret != "" { + f.mapSecretValues(parent, parent.SecretRef, parent.MountedSecret) + } + + if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { + f.mapSecretValues(config, config.SecretRef, config.MountedSecret) + } + + if config.Config.Webhook == "" && config.Config.Channel == "" { + return nil + } + + setFallback(&config.Config.Webhook, parent.Config.Webhook) + + if config.Config.Webhook == "" { + return nil + } + + config.MapBaseParent(parent) + + zap.S().Infof("%s configured", config.Name) + + return &target.Target{ + ID: uuid.NewString(), + Type: target.Slack, + Config: config, + ParentConfig: parent, + Client: slack.NewClient(slack.Options{ + ClientOptions: target.ClientOptions{ + Name: config.Name, + SkipExistingOnStartup: config.SkipExisting, + ResultFilter: f.createResultFilter(config.Filter, config.MinimumSeverity, config.Sources), + ReportFilter: createReportFilter(config.Filter), + }, + Channel: config.Config.Channel, + Webhook: config.Config.Webhook, + CustomFields: config.CustomFields, + Headers: config.Config.Headers, + HTTPClient: http.NewClient("", false), + }), + } +} + +func (f *TargetFactory) CreateLokiTarget(config, parent *target.Config[target.LokiOptions]) *target.Target { + if config == nil || config.Config == nil { + return nil + } + + if (parent.SecretRef != "" && f.secretClient != nil) || parent.MountedSecret != "" { + f.mapSecretValues(parent, parent.SecretRef, parent.MountedSecret) + } + + if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { + f.mapSecretValues(config, config.SecretRef, config.MountedSecret) + } + + if config.Config.Host == "" && parent.Config.Host == "" { + return nil + } + + setFallback(&config.Config.Path, "/loki/api/v1/push") + setFallback(&config.Config.Host, parent.Config.Host) + setFallback(&config.Config.Certificate, parent.Config.Certificate) + setFallback(&config.Config.Path, parent.Config.Path) + setFallback(&config.Config.Username, parent.Config.Username) + setFallback(&config.Config.Password, parent.Config.Password) + setBool(&config.Config.SkipTLS, parent.Config.SkipTLS) + + config.MapBaseParent(parent) + + zap.S().Infof("%s configured", config.Name) + + return &target.Target{ + ID: uuid.NewString(), + Type: target.Loki, + Config: config, + ParentConfig: parent, + Client: loki.NewClient(loki.Options{ + ClientOptions: target.ClientOptions{ + Name: config.Name, + SkipExistingOnStartup: config.SkipExisting, + ResultFilter: f.createResultFilter(config.Filter, config.MinimumSeverity, config.Sources), + ReportFilter: createReportFilter(config.Filter), + }, + Host: config.Config.Host + config.Config.Path, + CustomFields: config.CustomFields, + Username: config.Config.Username, + Password: config.Config.Password, + HTTPClient: http.NewClient(config.Config.Certificate, config.Config.SkipTLS), + Headers: config.Config.Headers, + }), + } +} + +func (f *TargetFactory) CreateElasticsearchTarget(config, parent *target.Config[target.ElasticsearchOptions]) *target.Target { + if config == nil || config.Config == nil { + return nil + } + + if (parent.SecretRef != "" && f.secretClient != nil) || parent.MountedSecret != "" { + f.mapSecretValues(parent, parent.SecretRef, parent.MountedSecret) + } + + if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { + f.mapSecretValues(config, config.SecretRef, config.MountedSecret) + } + + if config.Config.Host == "" && parent.Config.Host == "" { + return nil + } + + setFallback(&config.Config.Host, parent.Config.Host) + setFallback(&config.Config.Certificate, parent.Config.Certificate) + setBool(&config.Config.SkipTLS, parent.Config.SkipTLS) + setFallback(&config.Config.Username, parent.Config.Username) + setFallback(&config.Config.Password, parent.Config.Password) + setFallback(&config.Config.APIKey, parent.Config.APIKey) + setFallback(&config.Config.Index, parent.Config.Index, "policy-reporter") + setFallback(&config.Config.Rotation, parent.Config.Rotation, elasticsearch.Daily) + setBool(&config.Config.TypelessAPI, parent.Config.TypelessAPI) + + config.MapBaseParent(parent) + + zap.S().Infof("%s configured", config.Name) + + return &target.Target{ + ID: uuid.NewString(), + Type: target.Elasticsearch, + Config: config, + ParentConfig: parent, + Client: elasticsearch.NewClient(elasticsearch.Options{ + ClientOptions: target.ClientOptions{ + Name: config.Name, + SkipExistingOnStartup: config.SkipExisting, + ResultFilter: f.createResultFilter(config.Filter, config.MinimumSeverity, config.Sources), + ReportFilter: createReportFilter(config.Filter), + }, + Host: config.Config.Host, + Username: config.Config.Username, + Password: config.Config.Password, + ApiKey: config.Config.APIKey, + Rotation: config.Config.Rotation, + Index: config.Config.Index, + TypelessApi: config.Config.TypelessAPI, + CustomFields: config.CustomFields, + HTTPClient: http.NewClient(config.Config.Certificate, config.Config.SkipTLS), + }), + } +} + +func (f *TargetFactory) CreateDiscordTarget(config, parent *target.Config[target.WebhookOptions]) *target.Target { + if config == nil || config.Config == nil { + return nil + } + + if (parent.SecretRef != "" && f.secretClient != nil) || parent.MountedSecret != "" { + f.mapSecretValues(parent, parent.SecretRef, parent.MountedSecret) + } + + if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { + f.mapSecretValues(config, config.SecretRef, config.MountedSecret) + } + + mapWebhookTarget(config, parent) + + if config.Config.Webhook == "" { + return nil + } + + zap.S().Infof("%s configured", config.Name) + + return &target.Target{ + ID: uuid.NewString(), + Type: target.Discord, + Config: config, + ParentConfig: parent, + Client: discord.NewClient(discord.Options{ + ClientOptions: target.ClientOptions{ + Name: config.Name, + SkipExistingOnStartup: config.SkipExisting, + ResultFilter: f.createResultFilter(config.Filter, config.MinimumSeverity, config.Sources), + ReportFilter: createReportFilter(config.Filter), + }, + Webhook: config.Config.Webhook, + CustomFields: config.CustomFields, + HTTPClient: http.NewClient(config.Config.Certificate, config.Config.SkipTLS), + }), + } +} + +func (f *TargetFactory) CreateTeamsTarget(config, parent *target.Config[target.WebhookOptions]) *target.Target { + if config == nil || config.Config == nil { + return nil + } + + if (parent.SecretRef != "" && f.secretClient != nil) || parent.MountedSecret != "" { + f.mapSecretValues(parent, parent.SecretRef, parent.MountedSecret) + } + + if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { + f.mapSecretValues(config, config.SecretRef, config.MountedSecret) + } + + mapWebhookTarget(config, parent) + + if config.Config.Webhook == "" { + return nil + } + + zap.S().Infof("%s configured", config.Name) + + return &target.Target{ + ID: uuid.NewString(), + Type: target.Teams, + Config: config, + ParentConfig: parent, + Client: teams.NewClient(teams.Options{ + ClientOptions: target.ClientOptions{ + Name: config.Name, + SkipExistingOnStartup: config.SkipExisting, + ResultFilter: f.createResultFilter(config.Filter, config.MinimumSeverity, config.Sources), + ReportFilter: createReportFilter(config.Filter), + }, + Webhook: config.Config.Webhook, + CustomFields: config.CustomFields, + Headers: config.Config.Headers, + HTTPClient: http.NewClient(config.Config.Certificate, config.Config.SkipTLS), + }), + } +} + +func (f *TargetFactory) CreateWebhookTarget(config, parent *target.Config[target.WebhookOptions]) *target.Target { + if config == nil || config.Config == nil { + return nil + } + + if (parent.SecretRef != "" && f.secretClient != nil) || parent.MountedSecret != "" { + f.mapSecretValues(parent, parent.SecretRef, parent.MountedSecret) + } + + if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { + f.mapSecretValues(config, config.SecretRef, config.MountedSecret) + } + + mapWebhookTarget(config, parent) + + if config.Config.Webhook == "" { + return nil + } + + zap.S().Infof("%s configured", config.Name) + + return &target.Target{ + ID: uuid.NewString(), + Type: target.Webhook, + Config: config, + ParentConfig: parent, + Client: webhook.NewClient(webhook.Options{ + ClientOptions: target.ClientOptions{ + Name: config.Name, + SkipExistingOnStartup: config.SkipExisting, + ResultFilter: f.createResultFilter(config.Filter, config.MinimumSeverity, config.Sources), + ReportFilter: createReportFilter(config.Filter), + }, + Host: config.Config.Webhook, + Headers: config.Config.Headers, + CustomFields: config.CustomFields, + HTTPClient: http.NewClient(config.Config.Certificate, config.Config.SkipTLS), + }), + } +} + +func (f *TargetFactory) CreateTelegramTarget(config, parent *target.Config[target.TelegramOptions]) *target.Target { + if config == nil { + return nil + } + + if (parent.SecretRef != "" && f.secretClient != nil) || parent.MountedSecret != "" { + f.mapSecretValues(parent, parent.SecretRef, parent.MountedSecret) + } + + if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { + f.mapSecretValues(config, config.SecretRef, config.MountedSecret) + } + + setFallback(&config.Config.Token, parent.Config.Token) + + if config.Config.ChatID == "" || config.Config.Token == "" { + return nil + } + + setFallback(&config.Config.Webhook, parent.Config.Webhook) + setFallback(&config.Config.Certificate, parent.Config.Certificate) + setBool(&config.Config.SkipTLS, parent.Config.SkipTLS) + + config.MapBaseParent(parent) + + if len(parent.Config.Headers) > 0 { + headers := map[string]string{} + for header, value := range parent.Config.Headers { + headers[header] = value + } + for header, value := range config.Config.Headers { + headers[header] = value + } + + config.Config.Headers = headers + } + + host := "https://api.telegram.org" + if config.Config.Webhook != "" { + host = strings.TrimSuffix(config.Config.Webhook, "/") + } + + zap.S().Infof("%s configured", config.Name) + + return &target.Target{ + ID: uuid.NewString(), + Type: target.Telegram, + Config: config, + ParentConfig: parent, + Client: telegram.NewClient(telegram.Options{ + ClientOptions: target.ClientOptions{ + Name: config.Name, + SkipExistingOnStartup: config.SkipExisting, + ResultFilter: f.createResultFilter(config.Filter, config.MinimumSeverity, config.Sources), + ReportFilter: createReportFilter(config.Filter), + }, + Host: fmt.Sprintf("%s/bot%s/sendMessage", host, config.Config.Token), + ChatID: config.Config.ChatID, + Headers: config.Config.Headers, + CustomFields: config.CustomFields, + HTTPClient: http.NewClient(config.Config.Certificate, config.Config.SkipTLS), + }), + } +} + +func (f *TargetFactory) CreateGoogleChatTarget(config, parent *target.Config[target.WebhookOptions]) *target.Target { + if config == nil || config.Config == nil { + return nil + } + + if (parent.SecretRef != "" && f.secretClient != nil) || parent.MountedSecret != "" { + f.mapSecretValues(parent, parent.SecretRef, parent.MountedSecret) + } + + if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { + f.mapSecretValues(config, config.SecretRef, config.MountedSecret) + } + + mapWebhookTarget(config, parent) + + if config.Config.Webhook == "" { + return nil + } + + zap.S().Infof("%s configured", config.Name) + + return &target.Target{ + ID: uuid.NewString(), + Type: target.GoogleChat, + Config: config, + ParentConfig: parent, + Client: googlechat.NewClient(googlechat.Options{ + ClientOptions: target.ClientOptions{ + Name: config.Name, + SkipExistingOnStartup: config.SkipExisting, + ResultFilter: f.createResultFilter(config.Filter, config.MinimumSeverity, config.Sources), + ReportFilter: createReportFilter(config.Filter), + }, + Webhook: config.Config.Webhook, + Headers: config.Config.Headers, + CustomFields: config.CustomFields, + HTTPClient: http.NewClient(config.Config.Certificate, config.Config.SkipTLS), + }), + } +} + +func (f *TargetFactory) CreateS3Target(config, parent *target.Config[target.S3Options]) *target.Target { + if config == nil || config.Config == nil { + return nil + } + + if (parent.SecretRef != "" && f.secretClient != nil) || parent.MountedSecret != "" { + f.mapSecretValues(parent, parent.SecretRef, parent.MountedSecret) + } + + if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { + f.mapSecretValues(config, config.SecretRef, config.MountedSecret) + } + + setFallback(&config.Config.Bucket, parent.Config.Bucket) + if config.Config.Bucket == "" { + return nil + } + + config.Config.MapAWSParent(parent.Config.AWSConfig) + if config.Config.Endpoint == "" && !hasAWSIdentity() { + return nil + } + + sugar := zap.S() + + if err := checkAWSConfig(config.Name, config.Config.AWSConfig, parent.Config.AWSConfig); err != nil { + sugar.Error(err) + + return nil + } + + setFallback(&config.Config.Region, os.Getenv("AWS_REGION")) + setFallback(&config.Config.Prefix, parent.Config.Prefix, "policy-reporter") + setFallback(&config.Config.KmsKeyID, parent.Config.KmsKeyID) + setFallback(&config.Config.ServerSideEncryption, parent.Config.ServerSideEncryption) + setBool(&config.Config.BucketKeyEnabled, parent.Config.BucketKeyEnabled) + + config.MapBaseParent(parent) + + s3Client := aws.NewS3Client( + config.Config.AccessKeyID, + config.Config.SecretAccessKey, + config.Config.Region, + config.Config.Endpoint, + config.Config.Bucket, + config.Config.PathStyle, + aws.WithKMS(config.Config.BucketKeyEnabled, &config.Config.KmsKeyID, &config.Config.ServerSideEncryption), + ) + + sugar.Infof("%s configured", config.Name) + + return &target.Target{ + ID: uuid.NewString(), + Type: target.S3, + Config: config, + ParentConfig: parent, + Client: s3.NewClient(s3.Options{ + ClientOptions: target.ClientOptions{ + Name: config.Name, + SkipExistingOnStartup: config.SkipExisting, + ResultFilter: f.createResultFilter(config.Filter, config.MinimumSeverity, config.Sources), + ReportFilter: createReportFilter(config.Filter), + }, + S3: s3Client, + CustomFields: config.CustomFields, + Prefix: config.Config.Prefix, + }), + } +} + +func (f *TargetFactory) CreateKinesisTarget(config, parent *target.Config[target.KinesisOptions]) *target.Target { + if config == nil || config.Config == nil { + return nil + } + + if (parent.SecretRef != "" && f.secretClient != nil) || parent.MountedSecret != "" { + f.mapSecretValues(parent, parent.SecretRef, parent.MountedSecret) + } + + if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { + f.mapSecretValues(config, config.SecretRef, config.MountedSecret) + } + + config.Config.MapAWSParent(parent.Config.AWSConfig) + if config.Config.Endpoint == "" { + return nil + } + + sugar := zap.S() + if err := checkAWSConfig(config.Name, config.Config.AWSConfig, parent.Config.AWSConfig); err != nil { + sugar.Error(err) + + return nil + } + + setFallback(&config.Config.StreamName, parent.Config.StreamName) + if config.Config.StreamName == "" { + sugar.Errorf("%s.StreamName has not been declared", config.Name) + return nil + } + + setFallback(&config.Config.Region, os.Getenv("AWS_REGION")) + + config.MapBaseParent(parent) + + kinesisClient := aws.NewKinesisClient( + config.Config.AccessKeyID, + config.Config.SecretAccessKey, + config.Config.Region, + config.Config.Endpoint, + config.Config.StreamName, + ) + + sugar.Infof("%s configured", config.Name) + + return &target.Target{ + ID: uuid.NewString(), + Type: target.Kinesis, + Config: config, + ParentConfig: parent, + Client: kinesis.NewClient(kinesis.Options{ + ClientOptions: target.ClientOptions{ + Name: config.Name, + SkipExistingOnStartup: config.SkipExisting, + ResultFilter: f.createResultFilter(config.Filter, config.MinimumSeverity, config.Sources), + ReportFilter: createReportFilter(config.Filter), + }, + CustomFields: config.CustomFields, + Kinesis: kinesisClient, + }), + } +} + +func (f *TargetFactory) CreateSecurityHubTarget(config, parent *target.Config[target.SecurityHubOptions]) *target.Target { + if config == nil || config.Config == nil { + return nil + } + + if (parent.SecretRef != "" && f.secretClient != nil) || parent.MountedSecret != "" { + f.mapSecretValues(parent, parent.SecretRef, parent.MountedSecret) + } + + if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { + f.mapSecretValues(config, config.SecretRef, config.MountedSecret) + } + + setFallback(&config.Config.AccountID, parent.Config.AccountID) + if config.Config.AccountID == "" { + return nil + } + setFallback(&config.Config.Region, os.Getenv("AWS_REGION")) + + sugar := zap.S() + if err := checkAWSConfig(config.Name, config.Config.AWSConfig, parent.Config.AWSConfig); err != nil { + sugar.Error(err) + + return nil + } + + config.Config.MapAWSParent(parent.Config.AWSConfig) + config.MapBaseParent(parent) + + setFallback(&config.Config.ProductName, parent.Config.ProductName, "Policy Reporter") + setFallback(&config.Config.CompanyName, parent.Config.CompanyName, "Kyverno") + setInt(&config.Config.DelayInSeconds, parent.Config.DelayInSeconds) + + client := aws.NewHubClient( + config.Config.AccessKeyID, + config.Config.SecretAccessKey, + config.Config.Region, + config.Config.Endpoint, + ) + + zap.L().Info(config.Name+" configured", zap.Bool("synchronize", config.Config.Synchronize)) + + hub := securityhub.NewClient(securityhub.Options{ + ClientOptions: target.ClientOptions{ + Name: config.Name, + SkipExistingOnStartup: config.SkipExisting, + ResultFilter: f.createResultFilter(config.Filter, config.MinimumSeverity, config.Sources), + ReportFilter: createReportFilter(config.Filter), + }, + CustomFields: config.CustomFields, + Client: client, + AccountID: config.Config.AccountID, + ProductName: config.Config.ProductName, + CompanyName: config.Config.CompanyName, + Region: config.Config.Region, + Delay: time.Duration(config.Config.DelayInSeconds) * time.Second, + Synchronize: config.Config.Synchronize, + }) + + return &target.Target{ + ID: uuid.NewString(), + Type: target.SecurityHub, + Config: config, + ParentConfig: parent, + Client: hub, + } +} + +func (f *TargetFactory) CreateGCSTarget(config, parent *target.Config[target.GCSOptions]) *target.Target { + if config == nil || config.Config == nil { + return nil + } + + if (parent.SecretRef != "" && f.secretClient != nil) || parent.MountedSecret != "" { + f.mapSecretValues(parent, parent.SecretRef, parent.MountedSecret) + } + + if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { + f.mapSecretValues(config, config.SecretRef, config.MountedSecret) + } + + setFallback(&config.Config.Bucket, parent.Config.Bucket) + if config.Config.Bucket == "" { + return nil + } + + sugar := zap.S() + + setFallback(&config.Config.Credentials, parent.Config.Credentials) + if config.Config.Credentials == "" { + sugar.Errorf("%s.Credentials has not been declared", config.Name) + return nil + } + + setFallback(&config.Config.Prefix, parent.Config.Prefix, "policy-reporter") + + config.MapBaseParent(parent) + + gcsClient := gs.NewClient( + context.Background(), + config.Config.Credentials, + config.Config.Bucket, + ) + if gcsClient == nil { + return nil + } + + sugar.Infof("%s configured", config.Name) + return &target.Target{ + ID: uuid.NewString(), + Type: target.GCS, + Config: config, + ParentConfig: parent, + Client: gcs.NewClient(gcs.Options{ + ClientOptions: target.ClientOptions{ + Name: config.Name, + SkipExistingOnStartup: config.SkipExisting, + ResultFilter: f.createResultFilter(config.Filter, config.MinimumSeverity, config.Sources), + ReportFilter: createReportFilter(config.Filter), + }, + Client: gcsClient, + CustomFields: config.CustomFields, + Prefix: config.Config.Prefix, + }), + } +} + +func (f *TargetFactory) createResultFilter(filter target.Filter, minimumSeverity string, sources []string) *report.ResultFilter { + sourceFilter := filter.Sources + if len(sources) > 0 { + sourceFilter = target.ValueFilter{Include: sources} + } + + return f.filterFactory.CreateFilter( + validate.RuleSets{ + Include: filter.Namespaces.Include, + Exclude: filter.Namespaces.Exclude, + Selector: helper.ConvertMap(filter.Namespaces.Selector), + }, + ToRuleSet(filter.Severities), + ToRuleSet(filter.Status), + ToRuleSet(filter.Policies), + ToRuleSet(sourceFilter), + minimumSeverity, + ) +} + +func (f *TargetFactory) mapSecretValues(config any, ref, mountedSecret string) { + values := secrets.Values{} + + if ref != "" { + secretValues, err := f.secretClient.Get(context.Background(), ref) + values = secretValues + if err != nil { + zap.L().Warn("failed to get secret reference", zap.Error(err)) + return + } + } + + if mountedSecret != "" { + file, err := os.ReadFile(mountedSecret) + if err != nil { + zap.L().Warn("failed to get mounted secret", zap.Error(err)) + return + } + err = json.Unmarshal(file, &values) + if err != nil { + zap.L().Warn("failed to unmarshal mounted secret", zap.Error(err)) + return + } + } + + switch c := config.(type) { + case *target.Config[target.LokiOptions]: + if values.Host != "" { + c.Config.Host = values.Host + } + + case *target.Config[target.SlackOptions]: + if values.Webhook != "" { + c.Config.Webhook = values.Webhook + } + if values.Channel != "" { + c.Config.Channel = values.Channel + } + + case *target.Config[target.WebhookOptions]: + if values.Webhook != "" { + c.Config.Webhook = values.Webhook + } + if values.Token != "" { + if c.Config.Headers == nil { + c.Config.Headers = make(map[string]string) + } + + c.Config.Headers["Authorization"] = values.Token + } + + case *target.Config[target.ElasticsearchOptions]: + if values.Host != "" { + c.Config.Host = values.Host + } + if values.Username != "" { + c.Config.Username = values.Username + } + if values.Password != "" { + c.Config.Password = values.Password + } + if values.APIKey != "" { + c.Config.APIKey = values.APIKey + } + + case *target.Config[target.S3Options]: + if values.AccessKeyID != "" { + c.Config.AccessKeyID = values.AccessKeyID + } + if values.SecretAccessKey != "" { + c.Config.SecretAccessKey = values.SecretAccessKey + } + if values.KmsKeyID != "" { + c.Config.KmsKeyID = values.KmsKeyID + } + + case *target.Config[target.KinesisOptions]: + if values.AccessKeyID != "" { + c.Config.AccessKeyID = values.AccessKeyID + } + if values.SecretAccessKey != "" { + c.Config.SecretAccessKey = values.SecretAccessKey + } + + case *target.Config[target.SecurityHubOptions]: + if values.AccessKeyID != "" { + c.Config.AccessKeyID = values.AccessKeyID + } + if values.SecretAccessKey != "" { + c.Config.SecretAccessKey = values.SecretAccessKey + } + if values.AccountID != "" { + c.Config.AccountID = values.AccountID + } + + case *target.Config[target.GCSOptions]: + if values.Credentials != "" { + c.Config.Credentials = values.Credentials + } + + case *target.Config[target.TelegramOptions]: + if values.Token != "" { + c.Config.Token = values.Token + } + if values.Host != "" { + c.Config.Webhook = values.Host + } + } +} + +func NewFactory(secretClient secrets.Client, filterFactory *target.ResultFilterFactory) target.Factory { + return &TargetFactory{secretClient: secretClient, filterFactory: filterFactory} +} + +func mapWebhookTarget(config, parent *target.Config[target.WebhookOptions]) { + setFallback(&config.Config.Webhook, parent.Config.Webhook) + setFallback(&config.Config.Certificate, parent.Config.Certificate) + setBool(&config.Config.SkipTLS, parent.Config.SkipTLS) + + config.MapBaseParent(parent) + + if len(parent.Config.Headers) > 0 { + headers := map[string]string{} + for header, value := range parent.Config.Headers { + headers[header] = value + } + for header, value := range config.Config.Headers { + headers[header] = value + } + + config.Config.Headers = headers + } +} + +func hasAWSIdentity() bool { + irsaARN := os.Getenv("AWS_ROLE_ARN") + irsaFile := os.Getenv("AWS_WEB_IDENTITY_TOKEN_FILE") + + podIdentityFile := os.Getenv("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE") + podIdentityURI := os.Getenv("AWS_CONTAINER_CREDENTIALS_FULL_URI") + + return (irsaARN != "" && irsaFile != "") || (podIdentityFile != "" && podIdentityURI != "") +} + +func checkAWSConfig(name string, config target.AWSConfig, parent target.AWSConfig) error { + noEnvConfig := !hasAWSIdentity() + + if noEnvConfig && (config.AccessKeyID == "" && parent.AccessKeyID == "") { + return fmt.Errorf("%s.AccessKeyID has not been declared", name) + } + + if noEnvConfig && (config.SecretAccessKey == "" && parent.SecretAccessKey == "") { + return fmt.Errorf("%s.SecretAccessKey has not been declared", name) + } + + if config.Region == "" && parent.Region == "" { + return fmt.Errorf("%s.Region has not been declared", name) + } + + return nil +} + +func setFallback(config *string, parents ...string) { + if *config == "" { + for _, p := range parents { + if p != "" { + *config = p + return + } + } + } +} + +func setBool(config *bool, parent bool) { + if *config == false { + *config = parent + } +} + +func setInt(config *int, parent int) { + if *config == 0 { + *config = parent + } +} + +func createReportFilter(filter target.Filter) *report.ReportFilter { + return target.NewReportFilter( + ToRuleSet(filter.ReportLabels), + ToRuleSet(filter.Sources), + ) +} + +func ToRuleSet(filter target.ValueFilter) validate.RuleSets { + return validate.RuleSets{ + Include: filter.Include, + Exclude: filter.Exclude, + } +} diff --git a/pkg/target/factory/factory_test.go b/pkg/target/factory/factory_test.go new file mode 100644 index 00000000..0af34ddb --- /dev/null +++ b/pkg/target/factory/factory_test.go @@ -0,0 +1,1006 @@ +package factory_test + +import ( + "encoding/json" + "os" + "reflect" + "testing" + + "go.uber.org/zap" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" + v1 "k8s.io/client-go/kubernetes/typed/core/v1" + + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" + "github.com/kyverno/policy-reporter/pkg/kubernetes/secrets" + "github.com/kyverno/policy-reporter/pkg/target" + "github.com/kyverno/policy-reporter/pkg/target/factory" +) + +const ( + secretName = "secret-values" + mountedSecret = "/tmp/secrets-9999" +) + +func newFakeClient() v1.SecretInterface { + return fake.NewSimpleClientset(&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: "default", + }, + Data: map[string][]byte{ + "host": []byte("http://localhost:9200"), + "username": []byte("username"), + "password": []byte("password"), + "channel": []byte("general"), + "apiKey": []byte("apiKey"), + "webhook": []byte("http://localhost:9200/webhook"), + "accountId": []byte("accountId"), + "typelessApi": []byte("true"), + "accessKeyId": []byte("accessKeyId"), + "secretAccessKey": []byte("secretAccessKey"), + "kmsKeyId": []byte("kmsKeyId"), + "token": []byte("token"), + "credentials": []byte(`{"token": "token", "type": "authorized_user"}`), + "database": []byte("database"), + "dsn": []byte(""), + }, + }).CoreV1().Secrets("default") +} + +func mountSecret() { + secretValues := secrets.Values{ + Host: "http://localhost:9200", + Webhook: "http://localhost:9200/webhook", + Channel: "general", + Username: "username", + Password: "password", + APIKey: "apiKey", + AccountID: "accountId", + AccessKeyID: "accessKeyId", + SecretAccessKey: "secretAccessKey", + KmsKeyID: "kmsKeyId", + Token: "token", + Credentials: `{"token": "token", "type": "authorized_user"}`, + Database: "database", + TypelessAPI: true, + DSN: "", + } + file, _ := json.MarshalIndent(secretValues, "", " ") + _ = os.WriteFile(mountedSecret, file, 0o644) +} + +var logger = zap.NewNop() + +var targets = target.Targets{ + Loki: &target.Config[target.LokiOptions]{ + Config: &target.LokiOptions{ + HostOptions: target.HostOptions{ + Host: "http://localhost:3100", + SkipTLS: true, + }, + }, + SkipExisting: true, + MinimumSeverity: v1alpha2.SeverityInfo, + CustomFields: map[string]string{"field": "value"}, + Channels: []*target.Config[target.LokiOptions]{ + { + CustomFields: map[string]string{"label2": "value2"}, + }, + }, + }, + Elasticsearch: &target.Config[target.ElasticsearchOptions]{ + Config: &target.ElasticsearchOptions{ + HostOptions: target.HostOptions{ + Host: "http://localhost:9200", + SkipTLS: true, + }, + Index: "policy-reporter", + Rotation: "daily", + }, + SkipExisting: true, + MinimumSeverity: v1alpha2.SeverityInfo, + CustomFields: map[string]string{"field": "value"}, + Channels: []*target.Config[target.ElasticsearchOptions]{{}}, + }, + Slack: &target.Config[target.SlackOptions]{ + Config: &target.SlackOptions{ + WebhookOptions: target.WebhookOptions{ + Webhook: "http://localhost:80", + SkipTLS: true, + }, + }, + SkipExisting: true, + MinimumSeverity: v1alpha2.SeverityInfo, + CustomFields: map[string]string{"field": "value"}, + Channels: []*target.Config[target.SlackOptions]{{ + Config: &target.SlackOptions{ + WebhookOptions: target.WebhookOptions{ + Webhook: "http://localhost:9200", + }, + }, + }, { + Config: &target.SlackOptions{ + Channel: "general", + }, + }}, + }, + Discord: &target.Config[target.WebhookOptions]{ + Config: &target.WebhookOptions{ + Webhook: "http://discord:80", + SkipTLS: true, + }, + SkipExisting: true, + MinimumSeverity: v1alpha2.SeverityInfo, + CustomFields: map[string]string{"field": "value"}, + Channels: []*target.Config[target.WebhookOptions]{{ + Config: &target.WebhookOptions{ + Webhook: "http://localhost:9200", + }, + }}, + }, + Teams: &target.Config[target.WebhookOptions]{ + Config: &target.WebhookOptions{ + Webhook: "http://hook.teams:80", + SkipTLS: true, + }, + SkipExisting: true, + MinimumSeverity: v1alpha2.SeverityInfo, + CustomFields: map[string]string{"field": "value"}, + Channels: []*target.Config[target.WebhookOptions]{{ + Config: &target.WebhookOptions{ + Webhook: "http://localhost:9200", + }, + }}, + }, + GoogleChat: &target.Config[target.WebhookOptions]{ + Config: &target.WebhookOptions{ + Webhook: "http://localhost:900/webhook", + SkipTLS: true, + }, + SkipExisting: true, + MinimumSeverity: v1alpha2.SeverityInfo, + CustomFields: map[string]string{"field": "value"}, + Channels: []*target.Config[target.WebhookOptions]{{}}, + }, + Telegram: &target.Config[target.TelegramOptions]{ + Config: &target.TelegramOptions{ + WebhookOptions: target.WebhookOptions{ + Webhook: "http://localhost:80", + SkipTLS: true, + }, + Token: "XXX", + ChatID: "123456", + }, + SkipExisting: true, + MinimumSeverity: v1alpha2.SeverityInfo, + CustomFields: map[string]string{"field": "value"}, + Channels: []*target.Config[target.TelegramOptions]{{ + Config: &target.TelegramOptions{ + ChatID: "1234567", + }, + }}, + }, + Webhook: &target.Config[target.WebhookOptions]{ + Config: &target.WebhookOptions{ + Webhook: "http://localhost:8080", + SkipTLS: true, + Headers: map[string]string{ + "X-Custom": "Header", + }, + }, + SkipExisting: true, + MinimumSeverity: v1alpha2.SeverityInfo, + CustomFields: map[string]string{"field": "value"}, + Channels: []*target.Config[target.WebhookOptions]{{ + Config: &target.WebhookOptions{ + Webhook: "http://localhost:8081", + Headers: map[string]string{ + "X-Custom-2": "Header", + }, + }, + }}, + }, + S3: &target.Config[target.S3Options]{ + Config: &target.S3Options{ + AWSConfig: target.AWSConfig{ + AccessKeyID: "AccessKey", + SecretAccessKey: "SecretAccessKey", + Endpoint: "https://storage.yandexcloud.net", + Region: "ru-central1", + }, + Bucket: "test", + BucketKeyEnabled: false, + KmsKeyID: "", + ServerSideEncryption: "", + PathStyle: true, + Prefix: "prefix", + }, + SkipExisting: true, + MinimumSeverity: v1alpha2.SeverityInfo, + CustomFields: map[string]string{"field": "value"}, + Channels: []*target.Config[target.S3Options]{{}}, + }, + Kinesis: &target.Config[target.KinesisOptions]{ + Config: &target.KinesisOptions{ + AWSConfig: target.AWSConfig{ + AccessKeyID: "AccessKey", + SecretAccessKey: "SecretAccessKey", + Endpoint: "https://storage.yandexcloud.net", + Region: "ru-central1", + }, + StreamName: "policy-reporter", + }, + SkipExisting: true, + MinimumSeverity: v1alpha2.SeverityInfo, + CustomFields: map[string]string{"field": "value"}, + Channels: []*target.Config[target.KinesisOptions]{{}}, + }, + SecurityHub: &target.Config[target.SecurityHubOptions]{ + Config: &target.SecurityHubOptions{ + AWSConfig: target.AWSConfig{ + AccessKeyID: "AccessKey", + SecretAccessKey: "SecretAccessKey", + Endpoint: "https://storage.yandexcloud.net", + Region: "ru-central1", + }, + AccountID: "AccountID", + }, + SkipExisting: true, + MinimumSeverity: v1alpha2.SeverityInfo, + CustomFields: map[string]string{"field": "value"}, + Channels: []*target.Config[target.SecurityHubOptions]{{}}, + }, + GCS: &target.Config[target.GCSOptions]{ + Config: &target.GCSOptions{ + Credentials: `{"token": "token", "type": "authorized_user"}`, + Bucket: "test", + Prefix: "prefix", + }, + SkipExisting: true, + MinimumSeverity: v1alpha2.SeverityInfo, + CustomFields: map[string]string{"field": "value"}, + Channels: []*target.Config[target.GCSOptions]{{}}, + }, +} + +func Test_ResolveTarget(t *testing.T) { + factory := factory.NewFactory(nil, nil) + + clients := factory.CreateClients(&targets) + if len(clients.Clients()) != 25 { + t.Errorf("Expected 25 Client, got %d clients", len(clients.Clients())) + } +} + +func Test_ResolveTargetsWithoutRequiredConfiguration(t *testing.T) { + factory := factory.NewFactory(nil, nil) + + targets := target.Targets{ + Loki: &target.Config[target.LokiOptions]{}, + Elasticsearch: &target.Config[target.ElasticsearchOptions]{}, + Slack: &target.Config[target.SlackOptions]{}, + Discord: &target.Config[target.WebhookOptions]{}, + Teams: &target.Config[target.WebhookOptions]{}, + GoogleChat: &target.Config[target.WebhookOptions]{}, + Webhook: &target.Config[target.WebhookOptions]{}, + Telegram: &target.Config[target.TelegramOptions]{}, + S3: &target.Config[target.S3Options]{}, + Kinesis: &target.Config[target.KinesisOptions]{}, + SecurityHub: &target.Config[target.SecurityHubOptions]{}, + } + + if len(factory.CreateClients(&targets).Clients()) != 0 { + t.Error("Expected Client to be nil if no required fields are configured") + } + + targets = target.Targets{} + if len(factory.CreateClients(&targets).Clients()) != 0 { + t.Error("Expected Client to be nil if no target is configured") + } + + targets.S3 = &target.Config[target.S3Options]{ + Config: &target.S3Options{ + AWSConfig: target.AWSConfig{Endpoint: "https://storage.yandexcloud.net"}, + }, + } +} + +func Test_S3Validation(t *testing.T) { + factory := factory.NewFactory(nil, nil) + + targets := target.Targets{ + S3: &target.Config[target.S3Options]{ + Config: &target.S3Options{ + AWSConfig: target.AWSConfig{Endpoint: "https://storage.yandexcloud.net"}, + }, + }, + } + + t.Run("S3.AccessKey", func(t *testing.T) { + if len(factory.CreateClients(&targets).Clients()) != 0 { + t.Error("Expected Client to be nil if no accessKey is configured") + } + }) + + targets.S3.Config.AWSConfig.AccessKeyID = "access" + t.Run("S3.SecretAccessKey", func(t *testing.T) { + if len(factory.CreateClients(&targets).Clients()) != 0 { + t.Error("Expected Client to be nil if no secretAccessKey is configured") + } + }) + + targets.S3.Config.AWSConfig.SecretAccessKey = "secret" + t.Run("S3.Region", func(t *testing.T) { + if len(factory.CreateClients(&targets).Clients()) != 0 { + t.Error("Expected Client to be nil if no region is configured") + } + }) + + targets.S3.Config.AWSConfig.Region = "ru-central1" + t.Run("S3.Bucket", func(t *testing.T) { + if len(factory.CreateClients(&targets).Clients()) != 0 { + t.Error("Expected Client to be nil if no bucket is configured") + } + }) + + targets.S3.Config.ServerSideEncryption = "AES256" + t.Run("S3.SSE-S3", func(t *testing.T) { + if len(factory.CreateClients(&targets).Clients()) != 0 { + t.Error("Expected Client to be nil if server side encryption is not configured") + } + }) + + targets.S3.Config.ServerSideEncryption = "aws:kms" + t.Run("S3.SSE-KMS", func(t *testing.T) { + if len(factory.CreateClients(&targets).Clients()) != 0 { + t.Error("Expected Client to be nil if server side encryption is not configured") + } + }) + + targets.S3.Config.BucketKeyEnabled = true + t.Run("S3.SSE-KMS-S3-KEY", func(t *testing.T) { + if len(factory.CreateClients(&targets).Clients()) != 0 { + t.Error("Expected Client to be nil if server side encryption is not configured") + } + }) + + targets.S3.Config.KmsKeyID = "kmsKeyId" + t.Run("S3.SSE-KMS-KEY-ID", func(t *testing.T) { + if len(factory.CreateClients(&targets).Clients()) != 0 { + t.Error("Expected Client to be nil if server side encryption is not configured") + } + }) +} + +func Test_KinesisValidation(t *testing.T) { + factory := factory.NewFactory(nil, nil) + + targets := target.Targets{ + Kinesis: &target.Config[target.KinesisOptions]{ + Config: &target.KinesisOptions{ + AWSConfig: target.AWSConfig{Endpoint: "https://storage.yandexcloud.net"}, + }, + }, + } + + t.Run("Kinesis.AccessKey", func(t *testing.T) { + if len(factory.CreateClients(&targets).Clients()) != 0 { + t.Error("Expected Client to be nil if no accessKey is configured") + } + }) + + targets.Kinesis.Config.AWSConfig.AccessKeyID = "access" + t.Run("Kinesis.SecretAccessKey", func(t *testing.T) { + if len(factory.CreateClients(&targets).Clients()) != 0 { + t.Error("Expected Client to be nil if no secretAccessKey is configured") + } + }) + + targets.Kinesis.Config.AWSConfig.SecretAccessKey = "secret" + + t.Run("Kinesis.Region", func(t *testing.T) { + if len(factory.CreateClients(&targets).Clients()) != 0 { + t.Error("Expected Client to be nil if no region is configured") + } + }) + + targets.Kinesis.Config.AWSConfig.Region = "ru-central1" + + t.Run("Kinesis.StreamName", func(t *testing.T) { + if len(factory.CreateClients(&targets).Clients()) != 0 { + t.Error("Expected Client to be nil if no stream name is configured") + } + }) +} + +func Test_SecurityHubValidation(t *testing.T) { + factory := factory.NewFactory(nil, nil) + + targets := target.Targets{ + SecurityHub: &target.Config[target.SecurityHubOptions]{ + Config: &target.SecurityHubOptions{ + AWSConfig: target.AWSConfig{Endpoint: "https://storage.yandexcloud.net"}, + }, + }, + } + + t.Run("SecurityHub.AccountId", func(t *testing.T) { + if len(factory.CreateClients(&targets).Clients()) != 0 { + t.Error("Expected Client to be nil if no accountId is configured") + } + }) + + targets.SecurityHub.Config.AccountID = "accountId" + t.Run("SecurityHub.AccessKey", func(t *testing.T) { + if len(factory.CreateClients(&targets).Clients()) != 0 { + t.Error("Expected Client to be nil if no accessKey is configured") + } + }) + + targets.SecurityHub.Config.AWSConfig.AccessKeyID = "access" + t.Run("SecurityHub.SecretAccessKey", func(t *testing.T) { + if len(factory.CreateClients(&targets).Clients()) != 0 { + t.Error("Expected Client to be nil if no secretAccessKey is configured") + } + }) + + targets.SecurityHub.Config.AWSConfig.SecretAccessKey = "secret" + t.Run("SecurityHub.Region", func(t *testing.T) { + if len(factory.CreateClients(&targets).Clients()) != 0 { + t.Error("Expected Client to be nil if no region is configured") + } + }) +} + +func Test_GCSValidation(t *testing.T) { + factory := factory.NewFactory(nil, nil) + + targets := target.Targets{ + GCS: &target.Config[target.GCSOptions]{ + Config: &target.GCSOptions{ + Credentials: "{}", + }, + }, + } + + t.Run("GCS.Bucket", func(t *testing.T) { + if len(factory.CreateClients(&targets).Clients()) != 0 { + t.Error("Expected Client to be nil if no bucket is configured") + } + }) + + targets.GCS.Config.Bucket = "policy-reporter" + t.Run("GCS.Credentials", func(t *testing.T) { + if len(factory.CreateClients(&targets).Clients()) != 0 { + t.Error("Expected Client to be nil if no accessKey is configured") + } + }) +} + +func Test_GetValuesFromSecret(t *testing.T) { + factory := factory.NewFactory(secrets.NewClient(newFakeClient()), nil) + + targets := target.Targets{ + Loki: &target.Config[target.LokiOptions]{SecretRef: secretName}, + Elasticsearch: &target.Config[target.ElasticsearchOptions]{SecretRef: secretName}, + Slack: &target.Config[target.SlackOptions]{SecretRef: secretName}, + Discord: &target.Config[target.WebhookOptions]{SecretRef: secretName}, + Teams: &target.Config[target.WebhookOptions]{SecretRef: secretName}, + GoogleChat: &target.Config[target.WebhookOptions]{SecretRef: secretName}, + Webhook: &target.Config[target.WebhookOptions]{SecretRef: secretName}, + Telegram: &target.Config[target.TelegramOptions]{ + SecretRef: secretName, + Config: &target.TelegramOptions{ + ChatID: "1234", + }, + }, + S3: &target.Config[target.S3Options]{ + SecretRef: secretName, + Config: &target.S3Options{ + AWSConfig: target.AWSConfig{Endpoint: "endoint", Region: "region"}, + Bucket: "bucket", + }, + }, + Kinesis: &target.Config[target.KinesisOptions]{ + SecretRef: secretName, + Config: &target.KinesisOptions{ + AWSConfig: target.AWSConfig{Endpoint: "endoint", Region: "region"}, + StreamName: "stream", + }, + }, + SecurityHub: &target.Config[target.SecurityHubOptions]{ + SecretRef: secretName, + Config: &target.SecurityHubOptions{ + AWSConfig: target.AWSConfig{Endpoint: "endoint", Region: "region"}, + AccountID: "accountId", + }, + }, + GCS: &target.Config[target.GCSOptions]{ + SecretRef: secretName, + Config: &target.GCSOptions{ + Bucket: "policy-reporter", + }, + }, + } + + clients := factory.CreateClients(&targets) + if len(clients.Clients()) != 12 { + t.Fatalf("expected 12 clients created, got %d", len(clients.Clients())) + } + + t.Run("Get Loki values from Secret", func(t *testing.T) { + fv := reflect.ValueOf(clients.Client("Loki")).Elem().FieldByName("host") + if v := fv.String(); v != "http://localhost:9200/loki/api/v1/push" { + t.Errorf("Expected host from secret, got %s", v) + } + }) + + t.Run("Get Elasticsearch values from Secret", func(t *testing.T) { + client := reflect.ValueOf(clients.Client("Elasticsearch")).Elem() + + host := client.FieldByName("host").String() + if host != "http://localhost:9200" { + t.Errorf("Expected host from secret, got %s", host) + } + + username := client.FieldByName("username").String() + if username != "username" { + t.Errorf("Expected username from secret, got %s", username) + } + + rotation := client.FieldByName("rotation").String() + if rotation != "daily" { + t.Errorf("Expected rotation from secret, got %s", rotation) + } + + index := client.FieldByName("index").String() + if index != "policy-reporter" { + t.Errorf("Expected rotation from secret, got %s", index) + } + + password := client.FieldByName("password").String() + if password != "password" { + t.Errorf("Expected password from secret, got %s", password) + } + + apiKey := client.FieldByName("apiKey").String() + if apiKey != "apiKey" { + t.Errorf("Expected apiKey from secret, got %s", apiKey) + } + }) + + t.Run("Get Slack values from Secret", func(t *testing.T) { + client := reflect.ValueOf(clients.Client("Slack")).Elem() + + webhook := client.FieldByName("channel").String() + if webhook != "general" { + t.Errorf("Expected channel from secret, got %s", webhook) + } + }) + + t.Run("Get Discord values from Secret", func(t *testing.T) { + client := reflect.ValueOf(clients.Client("Discord")).Elem() + + webhook := client.FieldByName("webhook").String() + if webhook != "http://localhost:9200/webhook" { + t.Errorf("Expected webhook from secret, got %s", webhook) + } + }) + + t.Run("Get MS Teams values from Secret", func(t *testing.T) { + client := reflect.ValueOf(clients.Client("Teams")).Elem() + + webhook := client.FieldByName("webhook").String() + if webhook != "http://localhost:9200/webhook" { + t.Errorf("Expected webhook from secret, got %s", webhook) + } + }) + + t.Run("Get GoogleChat Webhook from Secret", func(t *testing.T) { + client := reflect.ValueOf(clients.Client("GoogleChat")).Elem() + + host := client.FieldByName("webhook").String() + if host != "http://localhost:9200/webhook" { + t.Errorf("Expected host with token from secret, got %s", host) + } + }) + + t.Run("Get Telegram Token from Secret", func(t *testing.T) { + client := reflect.ValueOf(clients.Client("Telegram")).Elem() + + host := client.FieldByName("host").String() + if host != "http://localhost:9200/bottoken/sendMessage" { + t.Errorf("Expected host with token from secret, got %s", host) + } + }) + + t.Run("Get Webhook Authentication Token from Secret", func(t *testing.T) { + client := reflect.ValueOf(clients.Client("Webhook")).Elem() + + token := client.FieldByName("headers").MapIndex(reflect.ValueOf("Authorization")).String() + if token != "token" { + t.Errorf("Expected token from secret, got %s", token) + } + }) + + t.Run("Get none existing secret skips target", func(t *testing.T) { + clients := factory.CreateClients(&target.Targets{ + Loki: &target.Config[target.LokiOptions]{SecretRef: "not-exist"}, + }) + + if len(clients.Clients()) != 0 { + t.Error("Expected client are skipped") + } + }) +} + +func Test_CustomFields(t *testing.T) { + factory := factory.NewFactory(nil, nil) + + targets := &target.Targets{ + Loki: &target.Config[target.LokiOptions]{ + Config: &target.LokiOptions{ + HostOptions: target.HostOptions{ + Host: "http://localhost:3100", + }, + }, + CustomFields: map[string]string{"field": "value"}, + }, + Elasticsearch: &target.Config[target.ElasticsearchOptions]{ + Config: &target.ElasticsearchOptions{ + HostOptions: target.HostOptions{ + Host: "http://localhost:9200", + }, + }, + CustomFields: map[string]string{"field": "value"}, + }, + Slack: &target.Config[target.SlackOptions]{ + Config: &target.SlackOptions{ + WebhookOptions: target.WebhookOptions{ + Webhook: "http://localhost:80", + }, + }, + CustomFields: map[string]string{"field": "value"}, + }, + Discord: &target.Config[target.WebhookOptions]{ + Config: &target.WebhookOptions{ + Webhook: "http://discord:80", + }, + CustomFields: map[string]string{"field": "value"}, + }, + Teams: &target.Config[target.WebhookOptions]{ + Config: &target.WebhookOptions{ + Webhook: "http://hook.teams:80", + }, + CustomFields: map[string]string{"field": "value"}, + }, + GoogleChat: &target.Config[target.WebhookOptions]{ + Config: &target.WebhookOptions{ + Webhook: "http://localhost:900/webhook", + }, + CustomFields: map[string]string{"field": "value"}, + }, + Telegram: &target.Config[target.TelegramOptions]{ + Config: &target.TelegramOptions{ + WebhookOptions: target.WebhookOptions{ + Webhook: "http://localhost:80", + }, + Token: "XXX", + ChatID: "123456", + }, + CustomFields: map[string]string{"field": "value"}, + }, + Webhook: &target.Config[target.WebhookOptions]{ + Config: &target.WebhookOptions{ + Webhook: "http://localhost:8080", + }, + CustomFields: map[string]string{"field": "value"}, + }, + S3: &target.Config[target.S3Options]{ + Config: &target.S3Options{ + AWSConfig: target.AWSConfig{ + AccessKeyID: "AccessKey", + SecretAccessKey: "SecretAccessKey", + Endpoint: "https://storage.yandexcloud.net", + Region: "ru-central1", + }, + Bucket: "test", + }, + CustomFields: map[string]string{"field": "value"}, + }, + Kinesis: &target.Config[target.KinesisOptions]{ + Config: &target.KinesisOptions{ + AWSConfig: target.AWSConfig{ + AccessKeyID: "AccessKey", + SecretAccessKey: "SecretAccessKey", + Endpoint: "https://storage.yandexcloud.net", + Region: "ru-central1", + }, + StreamName: "policy-reporter", + }, + CustomFields: map[string]string{"field": "value"}, + }, + SecurityHub: &target.Config[target.SecurityHubOptions]{ + Config: &target.SecurityHubOptions{ + AWSConfig: target.AWSConfig{ + AccessKeyID: "AccessKey", + SecretAccessKey: "SecretAccessKey", + Endpoint: "https://storage.yandexcloud.net", + Region: "ru-central1", + }, + AccountID: "AccountId", + }, + CustomFields: map[string]string{"field": "value"}, + }, + GCS: &target.Config[target.GCSOptions]{ + Config: &target.GCSOptions{ + Credentials: `{"token": "token", "type": "authorized_user"}`, + Bucket: "test", + Prefix: "prefix", + }, + CustomFields: map[string]string{"field": "value"}, + }, + } + + clients := factory.CreateClients(targets) + + if len(clients.Clients()) != 12 { + t.Fatalf("expected 12 client created, got %d", len(clients.Clients())) + } + + t.Run("Get CustomFields from Loki", func(t *testing.T) { + client := reflect.ValueOf(clients.Client("Loki")).Elem() + + customFields := client.FieldByName("customFields").MapKeys() + if customFields[0].String() != "field" { + t.Errorf("Expected customFields are added") + } + }) + + t.Run("Get CustomFields from Elasticsearch", func(t *testing.T) { + client := reflect.ValueOf(clients.Client("Elasticsearch")).Elem() + + customFields := client.FieldByName("customFields").MapKeys() + if customFields[0].String() != "field" { + t.Errorf("Expected customFields are added") + } + }) + + t.Run("Get CustomFields from Slack", func(t *testing.T) { + client := reflect.ValueOf(clients.Client("Slack")).Elem() + + customFields := client.FieldByName("customFields").MapKeys() + if customFields[0].String() != "field" { + t.Errorf("Expected customFields are added") + } + }) + t.Run("Get CustomFields from Discord", func(t *testing.T) { + client := reflect.ValueOf(clients.Client("Discord")).Elem() + + customFields := client.FieldByName("customFields").MapKeys() + if customFields[0].String() != "field" { + t.Errorf("Expected customFields are added") + } + }) + t.Run("Get CustomFields from MS Teams", func(t *testing.T) { + client := reflect.ValueOf(clients.Client("Teams")).Elem() + + customFields := client.FieldByName("customFields").MapKeys() + if customFields[0].String() != "field" { + t.Errorf("Expected customFields are added") + } + }) + + t.Run("Get CustomFields from GoogleChat", func(t *testing.T) { + client := reflect.ValueOf(clients.Client("GoogleChat")).Elem() + + customFields := client.FieldByName("customFields").MapKeys() + if customFields[0].String() != "field" { + t.Errorf("Expected customFields are added") + } + }) + + t.Run("Get CustomFields from Telegram", func(t *testing.T) { + client := reflect.ValueOf(clients.Client("Telegram")).Elem() + + customFields := client.FieldByName("customFields").MapKeys() + if customFields[0].String() != "field" { + t.Errorf("Expected customFields are added") + } + }) + + t.Run("Get CustomFields from Webhook", func(t *testing.T) { + client := reflect.ValueOf(clients.Client("Webhook")).Elem() + + customFields := client.FieldByName("customFields").MapKeys() + if customFields[0].String() != "field" { + t.Errorf("Expected customFields are added") + } + }) + t.Run("Get CustomFields from S3", func(t *testing.T) { + client := reflect.ValueOf(clients.Client("S3")).Elem() + + customFields := client.FieldByName("customFields").MapKeys() + if customFields[0].String() != "field" { + t.Errorf("Expected customFields are added") + } + }) + t.Run("Get CustomFields from Kinesis", func(t *testing.T) { + client := reflect.ValueOf(clients.Client("Kinesis")).Elem() + + customFields := client.FieldByName("customFields").MapKeys() + if customFields[0].String() != "field" { + t.Errorf("Expected customFields are added") + } + }) + t.Run("Get CustomFields from GCS", func(t *testing.T) { + client := reflect.ValueOf(clients.Client("GoogleCloudStorage")).Elem() + + customFields := client.FieldByName("customFields").MapKeys() + if customFields[0].String() != "field" { + t.Errorf("Expected customFields are added") + } + }) +} + +func Test_GetValuesFromMountedSecret(t *testing.T) { + factory := factory.NewFactory(secrets.NewClient(newFakeClient()), nil) + + mountSecret() + defer os.Remove(mountedSecret) + + targets := target.Targets{ + Loki: &target.Config[target.LokiOptions]{MountedSecret: mountedSecret}, + Elasticsearch: &target.Config[target.ElasticsearchOptions]{MountedSecret: mountedSecret}, + Slack: &target.Config[target.SlackOptions]{MountedSecret: mountedSecret}, + Discord: &target.Config[target.WebhookOptions]{MountedSecret: mountedSecret}, + Teams: &target.Config[target.WebhookOptions]{MountedSecret: mountedSecret}, + GoogleChat: &target.Config[target.WebhookOptions]{MountedSecret: mountedSecret}, + Webhook: &target.Config[target.WebhookOptions]{MountedSecret: mountedSecret}, + Telegram: &target.Config[target.TelegramOptions]{ + MountedSecret: mountedSecret, + Config: &target.TelegramOptions{ + ChatID: "1234", + }, + }, + S3: &target.Config[target.S3Options]{ + MountedSecret: mountedSecret, + Config: &target.S3Options{ + AWSConfig: target.AWSConfig{Endpoint: "endoint", Region: "region"}, + Bucket: "bucket", + }, + }, + Kinesis: &target.Config[target.KinesisOptions]{ + MountedSecret: mountedSecret, + Config: &target.KinesisOptions{ + AWSConfig: target.AWSConfig{Endpoint: "endoint", Region: "region"}, + StreamName: "stream", + }, + }, + SecurityHub: &target.Config[target.SecurityHubOptions]{ + MountedSecret: mountedSecret, + Config: &target.SecurityHubOptions{ + AWSConfig: target.AWSConfig{Endpoint: "endoint", Region: "region"}, + AccountID: "accountId", + }, + }, + GCS: &target.Config[target.GCSOptions]{ + MountedSecret: mountedSecret, + Config: &target.GCSOptions{ + Bucket: "policy-reporter", + }, + }, + } + + clients := factory.CreateClients(&targets) + if len(clients.Clients()) != 12 { + t.Fatalf("expected 12 client created, got %d", len(clients.Clients())) + } + + t.Run("Get Loki values from Secret", func(t *testing.T) { + fv := reflect.ValueOf(clients.Client("Loki")).Elem().FieldByName("host") + if v := fv.String(); v != "http://localhost:9200/loki/api/v1/push" { + t.Errorf("Expected host from secret, got %s", v) + } + }) + + t.Run("Get Elasticsearch values from Secret", func(t *testing.T) { + client := reflect.ValueOf(clients.Client("Elasticsearch")).Elem() + + host := client.FieldByName("host").String() + if host != "http://localhost:9200" { + t.Errorf("Expected host from secret, got %s", host) + } + + username := client.FieldByName("username").String() + if username != "username" { + t.Errorf("Expected username from secret, got %s", username) + } + + rotation := client.FieldByName("rotation").String() + if rotation != "daily" { + t.Errorf("Expected rotation from secret, got %s", rotation) + } + + index := client.FieldByName("index").String() + if index != "policy-reporter" { + t.Errorf("Expected rotation from secret, got %s", index) + } + + password := client.FieldByName("password").String() + if password != "password" { + t.Errorf("Expected password from secret, got %s", password) + } + + apiKey := client.FieldByName("apiKey").String() + if apiKey != "apiKey" { + t.Errorf("Expected apiKey from secret, got %s", apiKey) + } + }) + + t.Run("Get Slack values from Secret", func(t *testing.T) { + client := reflect.ValueOf(clients.Client("Slack")).Elem() + + webhook := client.FieldByName("channel").String() + if webhook != "general" { + t.Errorf("Expected channel from secret, got %s", webhook) + } + }) + + t.Run("Get Discord values from Secret", func(t *testing.T) { + client := reflect.ValueOf(clients.Client("Discord")).Elem() + + webhook := client.FieldByName("webhook").String() + if webhook != "http://localhost:9200/webhook" { + t.Errorf("Expected webhook from secret, got %s", webhook) + } + }) + + t.Run("Get MS Teams values from Secret", func(t *testing.T) { + client := reflect.ValueOf(clients.Client("Teams")).Elem() + + webhook := client.FieldByName("webhook").String() + if webhook != "http://localhost:9200/webhook" { + t.Errorf("Expected webhook from secret, got %s", webhook) + } + }) + + t.Run("Get GoogleChat Webhook from Secret", func(t *testing.T) { + client := reflect.ValueOf(clients.Client("GoogleChat")).Elem() + + host := client.FieldByName("webhook").String() + if host != "http://localhost:9200/webhook" { + t.Errorf("Expected host with token from secret, got %s", host) + } + }) + + t.Run("Get Telegram Token from Secret", func(t *testing.T) { + client := reflect.ValueOf(clients.Client("Telegram")).Elem() + + host := client.FieldByName("host").String() + if host != "http://localhost:9200/bottoken/sendMessage" { + t.Errorf("Expected host with token from secret, got %s", host) + } + }) + + t.Run("Get Webhook Authentication Token from Secret", func(t *testing.T) { + client := reflect.ValueOf(clients.Client("Webhook")).Elem() + + token := client.FieldByName("headers").MapIndex(reflect.ValueOf("Authorization")).String() + if token != "token" { + t.Errorf("Expected token from secret, got %s", token) + } + }) + + t.Run("Get none existing secret skips target", func(t *testing.T) { + clients := factory.CreateClients(&target.Targets{ + Loki: &target.Config[target.LokiOptions]{SecretRef: "not-exist"}, + }) + + if len(clients.Clients()) != 0 { + t.Error("Expected client are skipped") + } + }) +} diff --git a/pkg/target/factory_test.go b/pkg/target/factory_test.go new file mode 100644 index 00000000..9a72e5b2 --- /dev/null +++ b/pkg/target/factory_test.go @@ -0,0 +1,96 @@ +package target_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" + "github.com/kyverno/policy-reporter/pkg/target" +) + +func TestConfig(t *testing.T) { + t.Run("return expected secret ref", func(t *testing.T) { + c := &target.Config[target.WebhookOptions]{ + SecretRef: "webhook-secret", + } + + assert.Equal(t, c.Secret(), "webhook-secret") + }) + + t.Run("ignores secret mount", func(t *testing.T) { + c := &target.Config[target.WebhookOptions]{ + MountedSecret: "webhook-secret", + } + + assert.Equal(t, c.Secret(), "") + }) + + t.Run("base mapper set expected fallbacks from parent config", func(t *testing.T) { + p := &target.Config[target.WebhookOptions]{ + MinimumSeverity: v1alpha2.SeverityMedium, + SkipExisting: true, + } + + c := &target.Config[target.WebhookOptions]{} + c.MapBaseParent(p) + + assert.Equal(t, c.MinimumSeverity, p.MinimumSeverity) + assert.Equal(t, c.SkipExisting, p.SkipExisting) + }) + + t.Run("base mapper keeps none empty values", func(t *testing.T) { + p := &target.Config[target.WebhookOptions]{ + MinimumSeverity: v1alpha2.SeverityMedium, + } + + c := &target.Config[target.WebhookOptions]{ + MinimumSeverity: v1alpha2.SeverityInfo, + } + + c.MapBaseParent(p) + + assert.Equal(t, c.MinimumSeverity, v1alpha2.SeverityInfo) + }) +} + +func TestAWSConfig(t *testing.T) { + t.Run("aws mapper set expected fallbacks from parent config", func(t *testing.T) { + p := target.AWSConfig{ + AccessKeyID: "access", + SecretAccessKey: "secret", + Region: "eu", + Endpoint: "http://localhost:8080", + } + + c := target.AWSConfig{} + c.MapAWSParent(p) + + assert.Equal(t, c.AccessKeyID, p.AccessKeyID) + assert.Equal(t, c.SecretAccessKey, p.SecretAccessKey) + assert.Equal(t, c.Region, p.Region) + assert.Equal(t, c.Endpoint, p.Endpoint) + }) + + t.Run("base mapper keeps none empty values", func(t *testing.T) { + p := target.AWSConfig{ + AccessKeyID: "access", + SecretAccessKey: "secret", + Region: "eu", + Endpoint: "http://localhost:8080", + } + + c := target.AWSConfig{ + AccessKeyID: "access_child", + SecretAccessKey: "secret_child", + Region: "de", + Endpoint: "http://localhost:9090", + } + c.MapAWSParent(p) + + assert.Equal(t, c.AccessKeyID, "access_child") + assert.Equal(t, c.SecretAccessKey, "secret_child") + assert.Equal(t, c.Region, "de") + assert.Equal(t, c.Endpoint, "http://localhost:9090") + }) +} diff --git a/pkg/target/formatting/resource.go b/pkg/target/formatting/resource.go new file mode 100644 index 00000000..19d3c05d --- /dev/null +++ b/pkg/target/formatting/resource.go @@ -0,0 +1,19 @@ +package formatting + +import ( + "fmt" + "strings" + + corev1 "k8s.io/api/core/v1" +) + +func ResourceString(res *corev1.ObjectReference) string { + var resource string + if res.Namespace == "" { + resource = fmt.Sprintf("%s/%s: %s", res.APIVersion, res.Kind, res.Name) + } else { + resource = fmt.Sprintf("%s/%s: %s/%s", res.APIVersion, res.Kind, res.Namespace, res.Name) + } + + return strings.Trim(resource, "/") +} diff --git a/pkg/target/gcs/gcs.go b/pkg/target/gcs/gcs.go index add51ce1..c6638245 100644 --- a/pkg/target/gcs/gcs.go +++ b/pkg/target/gcs/gcs.go @@ -2,7 +2,6 @@ package gcs import ( "bytes" - "context" "encoding/json" "fmt" "time" @@ -10,23 +9,23 @@ import ( "go.uber.org/zap" "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" - "github.com/kyverno/policy-reporter/pkg/helper" "github.com/kyverno/policy-reporter/pkg/target" "github.com/kyverno/policy-reporter/pkg/target/http" + "github.com/kyverno/policy-reporter/pkg/target/provider/gcs" ) // Options to configure the GCS target type Options struct { target.ClientOptions CustomFields map[string]string - Client helper.GCPClient + Client gcs.Client Prefix string } type client struct { target.BaseClient customFields map[string]string - client helper.GCPClient + client gcs.Client prefix string } @@ -63,7 +62,9 @@ func (c *client) Send(result v1alpha2.PolicyReportResult) { zap.L().Info(c.Name() + ": PUSH OK") } -func (c *client) CleanUp(_ context.Context, _ v1alpha2.ReportInterface) {} +func (c *client) Type() target.ClientType { + return target.SingleSend +} // NewClient creates a new GCS.client to send Results to Google Cloud Storage. func NewClient(options Options) target.Client { diff --git a/pkg/target/googlechat/googlechat.go b/pkg/target/googlechat/googlechat.go index fa75e7ca..46a67930 100644 --- a/pkg/target/googlechat/googlechat.go +++ b/pkg/target/googlechat/googlechat.go @@ -2,7 +2,6 @@ package googlechat import ( "bytes" - "context" "text/template" "time" @@ -15,7 +14,7 @@ import ( ) const ( - messageTempl string = `[{{ .Priority }}] {{ or .Result.Policy .Result.Rule }}` + messageTempl string = `[{{ .Result.Severity }}] {{ or .Result.Policy .Result.Rule }}` resourceTempl string = `{{ if .Namespace }}[{{ .Namespace }}] {{ end }} {{ .APIVersion }}/{{ .Kind }} {{ .Name }}` ) @@ -98,13 +97,13 @@ func mapPayload(result v1alpha2.PolicyReportResult) (*Payload, error) { return nil, err } - prio := result.Priority.String() + prio := result.Severity if prio == "" { - prio = v1alpha2.DebugPriority.String() + prio = v1alpha2.SeverityInfo } var textBuffer bytes.Buffer - err = ttmpl.Execute(&textBuffer, values{Result: result, Priority: prio, Resource: result.GetResource()}) + err = ttmpl.Execute(&textBuffer, values{Result: result, Resource: result.GetResource()}) if err != nil { return nil, err } @@ -207,7 +206,7 @@ func (e *client) Send(result v1alpha2.PolicyReportResult) { return } - req, err := http.CreateJSONRequest(e.Name(), "POST", e.webhook, payload) + req, err := http.CreateJSONRequest("POST", e.webhook, payload) if err != nil { return } @@ -220,7 +219,9 @@ func (e *client) Send(result v1alpha2.PolicyReportResult) { http.ProcessHTTPResponse(e.Name(), resp, err) } -func (e *client) CleanUp(_ context.Context, _ v1alpha2.ReportInterface) {} +func (e *client) Type() target.ClientType { + return target.SingleSend +} // NewClient creates a new loki.client to send Results to Elasticsearch func NewClient(options Options) target.Client { diff --git a/pkg/target/http/logroundtripper.go b/pkg/target/http/logroundtripper.go index 762c669e..231315ca 100644 --- a/pkg/target/http/logroundtripper.go +++ b/pkg/target/http/logroundtripper.go @@ -16,14 +16,13 @@ type logRoundTripper struct { roundTripper http.RoundTripper } +var _ http.RoundTripper = (*logRoundTripper)(nil) + func (rt *logRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { logger := zap.L() if logger.Core().Enabled(zap.DebugLevel) { if info, err := httputil.DumpRequest(req, true); err == nil { logger.Debug(fmt.Sprintf("Sending request: %s", string(info))) - if err != nil { - return nil, err - } } } resp, err := rt.roundTripper.RoundTrip(req) diff --git a/pkg/target/http/logroundtripper_test.go b/pkg/target/http/logroundtripper_test.go new file mode 100644 index 00000000..3afc24bd --- /dev/null +++ b/pkg/target/http/logroundtripper_test.go @@ -0,0 +1,33 @@ +package http_test + +import ( + net "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + "go.uber.org/zap/zaptest/observer" + + "github.com/kyverno/policy-reporter/pkg/target/http" +) + +type mock struct{} + +func (rt mock) RoundTrip(req *net.Request) (*net.Response, error) { + return httptest.NewRecorder().Result(), nil +} + +func TestDebug(t *testing.T) { + obs, logs := observer.New(zap.DebugLevel) + + zap.ReplaceGlobals(zap.New(obs)) + + r := http.NewLoggingRoundTripper(mock{}) + + _, err := r.RoundTrip(httptest.NewRequest("GET", "http://localhost:8080/healthz", nil)) + + assert.Nil(t, err) + + assert.Equal(t, 2, logs.FilterLevelExact(zap.DebugLevel).Len()) +} diff --git a/pkg/target/http/utils.go b/pkg/target/http/utils.go index 1f8b52f7..6ba56bc2 100644 --- a/pkg/target/http/utils.go +++ b/pkg/target/http/utils.go @@ -15,14 +15,14 @@ import ( ) // CreateJSONRequest for the given configuration -func CreateJSONRequest(target, method, host string, payload interface{}) (*http.Request, error) { +func CreateJSONRequest(method, host string, payload interface{}) (*http.Request, error) { body := new(bytes.Buffer) json.NewEncoder(body).Encode(payload) req, err := http.NewRequest(method, host, body) if err != nil { - zap.L().Error(target+": PUSH FAILED", zap.Error(err)) + zap.L().Error("failed to create request", zap.Error(err)) return nil, err } @@ -67,7 +67,6 @@ func NewJSONResult(r v1alpha2.PolicyReportResult) Result { Message: r.Message, Policy: r.Policy, Rule: r.Rule, - Priority: r.Priority.String(), Status: string(r.Result), Severity: string(r.Severity), Category: r.Category, diff --git a/pkg/target/http/utils_test.go b/pkg/target/http/utils_test.go new file mode 100644 index 00000000..18436fda --- /dev/null +++ b/pkg/target/http/utils_test.go @@ -0,0 +1,94 @@ +package http_test + +import ( + "encoding/json" + "errors" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + "go.uber.org/zap/zaptest/observer" + + "github.com/kyverno/policy-reporter/pkg/fixtures" + "github.com/kyverno/policy-reporter/pkg/target/http" +) + +func TestResultMapping(t *testing.T) { + result := http.NewJSONResult(fixtures.CompleteTargetSendResult) + + assert.Equal(t, result.Message, fixtures.CompleteTargetSendResult.Message) + assert.Equal(t, result.Policy, fixtures.CompleteTargetSendResult.Policy) + assert.Equal(t, result.Rule, fixtures.CompleteTargetSendResult.Rule) + assert.Equal(t, result.Resource.Name, fixtures.CompleteTargetSendResult.Resources[0].Name) +} + +func TestCreateJSONRequest(t *testing.T) { + t.Run("success", func(t *testing.T) { + req, err := http.CreateJSONRequest("GET", "http://localhost:8080", []string{"test"}) + + assert.Nil(t, err) + + list := make([]string, 0) + + json.NewDecoder(req.Body).Decode(&list) + + assert.Equal(t, []string{"test"}, list) + assert.Equal(t, "GET", req.Method) + assert.Equal(t, "application/json; charset=utf-8", req.Header.Get("Content-Type")) + assert.Equal(t, "Policy-Reporter", req.Header.Get("User-Agent")) + }) + + t.Run("error", func(t *testing.T) { + _, err := http.CreateJSONRequest("GET", "\test", []string{"test"}) + + assert.NotNil(t, err) + }) +} + +func TestClient(t *testing.T) { + assert.NotNil(t, http.NewClient("", true)) +} + +func TestProcessHTTPResponse(t *testing.T) { + t.Run("success", func(t *testing.T) { + obs, logs := observer.New(zap.InfoLevel) + + zap.ReplaceGlobals(zap.New(obs)) + + w := httptest.NewRecorder() + w.Write([]byte(`["test"]`)) + + http.ProcessHTTPResponse("Test", w.Result(), nil) + + assert.Equal(t, 1, logs.Len()) + assert.Equal(t, 1, logs.FilterLevelExact(zap.InfoLevel).Len()) + }) + t.Run("error", func(t *testing.T) { + obs, logs := observer.New(zap.InfoLevel) + + zap.ReplaceGlobals(zap.New(obs)) + + w := httptest.NewRecorder() + w.Write([]byte(`["test"]`)) + + http.ProcessHTTPResponse("Test", w.Result(), errors.New("error")) + + assert.Equal(t, 1, logs.Len()) + assert.Equal(t, 1, logs.FilterMessage("Test: PUSH FAILED").Len()) + }) + t.Run("error status code", func(t *testing.T) { + obs, logs := observer.New(zap.InfoLevel) + + zap.ReplaceGlobals(zap.New(obs)) + + w := httptest.NewRecorder() + resp := w.Result() + resp.StatusCode = 404 + + http.ProcessHTTPResponse("Test", w.Result(), nil) + + assert.Equal(t, 1, logs.Len()) + assert.Equal(t, 1, logs.FilterMessage("Test: PUSH FAILED").Len()) + }) +} diff --git a/pkg/target/kinesis/kinesis.go b/pkg/target/kinesis/kinesis.go index c365a613..04699a26 100644 --- a/pkg/target/kinesis/kinesis.go +++ b/pkg/target/kinesis/kinesis.go @@ -2,7 +2,6 @@ package kinesis import ( "bytes" - "context" "encoding/json" "fmt" "time" @@ -10,22 +9,22 @@ import ( "go.uber.org/zap" "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" - "github.com/kyverno/policy-reporter/pkg/helper" "github.com/kyverno/policy-reporter/pkg/target" "github.com/kyverno/policy-reporter/pkg/target/http" + "github.com/kyverno/policy-reporter/pkg/target/provider/aws" ) // Options to configure the Kinesis target type Options struct { target.ClientOptions CustomFields map[string]string - Kinesis helper.AWSClient + Kinesis aws.Client } type client struct { target.BaseClient customFields map[string]string - kinesis helper.AWSClient + kinesis aws.Client } func (c *client) Send(result v1alpha2.PolicyReportResult) { @@ -52,8 +51,7 @@ func (c *client) Send(result v1alpha2.PolicyReportResult) { t := time.Unix(result.Timestamp.Seconds, int64(result.Timestamp.Nanos)) key := fmt.Sprintf("%s-%s-%s", result.Policy, result.ID, t.Format(time.RFC3339Nano)) - err := c.kinesis.Upload(body, key) - if err != nil { + if err := c.kinesis.Upload(body, key); err != nil { zap.L().Error("kinesis upload error", zap.String("name", c.Name()), zap.Error(err)) return } @@ -61,7 +59,9 @@ func (c *client) Send(result v1alpha2.PolicyReportResult) { zap.L().Info("PUSH OK", zap.String("name", c.Name())) } -func (c *client) CleanUp(_ context.Context, _ v1alpha2.ReportInterface) {} +func (c *client) Type() target.ClientType { + return target.SingleSend +} // NewClient creates a new Kinesis.client to send Results to AWS Kinesis compatible source func NewClient(options Options) target.Client { diff --git a/pkg/target/loki/loki.go b/pkg/target/loki/loki.go index 5a1f6a2c..c2b8d3b6 100644 --- a/pkg/target/loki/loki.go +++ b/pkg/target/loki/loki.go @@ -1,108 +1,122 @@ package loki import ( - "context" + "fmt" "strings" "time" "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" + "github.com/kyverno/policy-reporter/pkg/helper" "github.com/kyverno/policy-reporter/pkg/target" "github.com/kyverno/policy-reporter/pkg/target/http" ) -// Options to configure the Loko target +var ( + keyReplacer = strings.NewReplacer(".", "_", "]", "", "[", "") + labelReplacer = strings.NewReplacer("/", "") +) + +// Options to configure the Loki target type Options struct { target.ClientOptions Host string - CustomLabels map[string]string + CustomFields map[string]string Headers map[string]string HTTPClient http.Client Username string Password string } -type payload struct { - Streams []stream `json:"streams"` +type Payload struct { + Streams []Stream `json:"streams"` } -type stream struct { - Labels string `json:"labels"` - Entries []entry `json:"entries"` +type Stream struct { + Stream map[string]string `json:"stream"` + Values []Value `json:"values"` } -type entry struct { - Ts string `json:"ts"` - Line string `json:"line"` -} +type Value = []string -func newLokiPayload(result v1alpha2.PolicyReportResult, customLabels map[string]string) payload { +func newLokiStream(result v1alpha2.PolicyReportResult, customFields map[string]string) Stream { timestamp := time.Now() if result.Timestamp.Seconds != 0 { timestamp = time.Unix(result.Timestamp.Seconds, int64(result.Timestamp.Nanos)) } - le := entry{Ts: timestamp.Format(time.RFC3339), Line: "[" + strings.ToUpper(result.Priority.String()) + "] " + result.Message} - ls := stream{Entries: []entry{le}} - - labels := []string{ - "status=\"" + string(result.Result) + "\"", - "policy=\"" + result.Policy + "\"", - "priority=\"" + result.Priority.String() + "\"", - "source=\"policy-reporter\"", + labels := map[string]string{ + "status": string(result.Result), + "policy": result.Policy, + "createdBy": "policy-reporter", } if result.Rule != "" { - labels = append(labels, "rule=\""+result.Rule+"\"") + labels["rule"] = result.Rule } if result.Category != "" { - labels = append(labels, "category=\""+result.Category+"\"") + labels["category"] = result.Category } if result.Severity != "" { - labels = append(labels, "severity=\""+string(result.Severity)+"\"") + labels["severity"] = string(result.Severity) } if result.Source != "" { - labels = append(labels, "producer=\""+result.Source+"\"") + labels["source"] = result.Source } if result.HasResource() { res := result.GetResource() if res.APIVersion != "" { - labels = append(labels, "apiVersion=\""+res.APIVersion+"\"") + labels["apiVersion"] = res.APIVersion + labels["kind"] = res.Kind + labels["name"] = res.Name } - labels = append(labels, "kind=\""+res.Kind+"\"") - labels = append(labels, "name=\""+res.Name+"\"") if res.UID != "" { - labels = append(labels, "uid=\""+string(res.UID)+"\"") + labels["uid"] = string(res.UID) } if res.Namespace != "" { - labels = append(labels, "namespace=\""+res.Namespace+"\"") + labels["namespace"] = res.Namespace } } for property, value := range result.Properties { - labels = append(labels, strings.ReplaceAll(property, ".", "_")+"=\""+strings.ReplaceAll(value, "\"", "")+"\"") + labels[keyReplacer.Replace(property)] = labelReplacer.Replace(value) } - for label, value := range customLabels { - labels = append(labels, strings.ReplaceAll(label, ".", "_")+"=\""+strings.ReplaceAll(value, "\"", "")+"\"") + for label, value := range customFields { + labels[keyReplacer.Replace(label)] = labelReplacer.Replace(value) } - ls.Labels = "{" + strings.Join(labels, ",") + "}" - - return payload{Streams: []stream{ls}} + return Stream{ + Values: []Value{[]string{fmt.Sprintf("%v", timestamp.UnixNano()), "[" + strings.ToUpper(string(result.Severity)) + "] " + result.Message}}, + Stream: labels, + } } type client struct { target.BaseClient host string client http.Client - customLabels map[string]string + customFields map[string]string headers map[string]string username string password string } func (l *client) Send(result v1alpha2.PolicyReportResult) { - req, err := http.CreateJSONRequest(l.Name(), "POST", l.host, newLokiPayload(result, l.customLabels)) + l.send(Payload{ + Streams: []Stream{ + newLokiStream(result, l.customFields), + }, + }) +} + +func (l *client) BatchSend(_ v1alpha2.ReportInterface, results []v1alpha2.PolicyReportResult) { + l.send(Payload{Streams: helper.Map(results, func(result v1alpha2.PolicyReportResult) Stream { + return newLokiStream(result, l.customFields) + })}) +} + +func (l *client) send(payload Payload) { + req, err := http.CreateJSONRequest("POST", l.host, payload) if err != nil { return } @@ -120,7 +134,9 @@ func (l *client) Send(result v1alpha2.PolicyReportResult) { http.ProcessHTTPResponse(l.Name(), resp, err) } -func (l *client) CleanUp(_ context.Context, _ v1alpha2.ReportInterface) {} +func (l *client) Type() target.ClientType { + return target.BatchSend +} // NewClient creates a new loki.client to send Results to Loki func NewClient(options Options) target.Client { @@ -128,7 +144,7 @@ func NewClient(options Options) target.Client { target.NewBaseClient(options.ClientOptions), options.Host, options.HTTPClient, - options.CustomLabels, + options.CustomFields, options.Headers, options.Username, options.Password, diff --git a/pkg/target/loki/loki_test.go b/pkg/target/loki/loki_test.go index 27624d02..96b101ba 100644 --- a/pkg/target/loki/loki_test.go +++ b/pkg/target/loki/loki_test.go @@ -7,6 +7,8 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/kyverno/policy-reporter/pkg/fixtures" "github.com/kyverno/policy-reporter/pkg/target" "github.com/kyverno/policy-reporter/pkg/target/loki" @@ -39,7 +41,7 @@ func Test_LokiTarget(t *testing.T) { t.Errorf("Unexpected Host: %s", agend) } - if url := req.URL.String(); url != "http://localhost:3100/api/prom/push" { + if url := req.URL.String(); url != "http://localhost:3100/loki/api/v1/push" { t.Errorf("Unexpected Host: %s", url) } @@ -47,60 +49,32 @@ func Test_LokiTarget(t *testing.T) { t.Error("Expected Authentication header for BasicAuth is set") } - expectedLine := fmt.Sprintf("[%s] %s", strings.ToUpper(fixtures.CompleteTargetSendResult.Priority.String()), fixtures.CompleteTargetSendResult.Message) - labels, line := convertAndValidateBody(req, t) - if line != expectedLine { - t.Errorf("Unexpected LineContent: %s", line) - } - if !strings.Contains(labels, "policy=\""+fixtures.CompleteTargetSendResult.Policy+"\"") { - t.Error("Missing Content for Label 'policy'") - } - if !strings.Contains(labels, "status=\""+string(fixtures.CompleteTargetSendResult.Result)+"\"") { - t.Error("Missing Content for Label 'status'") - } - if !strings.Contains(labels, "priority=\""+fixtures.CompleteTargetSendResult.Priority.String()+"\"") { - t.Error("Missing Content for Label 'priority'") - } - if !strings.Contains(labels, "source=\"policy-reporter\"") { - t.Error("Missing Content for Label 'policy-reporter'") - } - if !strings.Contains(labels, "rule=\""+fixtures.CompleteTargetSendResult.Rule+"\"") { - t.Error("Missing Content for Label 'rule'") - } - if !strings.Contains(labels, "category=\""+fixtures.CompleteTargetSendResult.Category+"\"") { - t.Error("Missing Content for Label 'category'") - } - if !strings.Contains(labels, "severity=\""+string(fixtures.CompleteTargetSendResult.Severity)+"\"") { - t.Error("Missing Content for Label 'severity'") - } - if !strings.Contains(labels, "custom=\"label\"") { - t.Error("Missing Content for Label 'severity'") - } + expectedLine := fmt.Sprintf("[%s] %s", strings.ToUpper(string(fixtures.CompleteTargetSendResult.Severity)), fixtures.CompleteTargetSendResult.Message) + + stream := convertAndValidateBody(req, t) + + assert.Equal(t, expectedLine, stream.Values[0][1]) + assert.Equal(t, fixtures.CompleteTargetSendResult.Rule, stream.Stream["rule"]) + assert.Equal(t, fixtures.CompleteTargetSendResult.Policy, stream.Stream["policy"]) + assert.Equal(t, fixtures.CompleteTargetSendResult.Category, stream.Stream["category"]) + assert.Equal(t, string(fixtures.CompleteTargetSendResult.Result), stream.Stream["status"]) + assert.Equal(t, string(fixtures.CompleteTargetSendResult.Severity), stream.Stream["severity"]) res := fixtures.CompleteTargetSendResult.GetResource() - if !strings.Contains(labels, "kind=\""+res.Kind+"\"") { - t.Error("Missing Content for Label 'kind'") - } - if !strings.Contains(labels, "name=\""+res.Name+"\"") { - t.Error("Missing Content for Label 'name'") - } - if !strings.Contains(labels, "uid=\""+string(res.UID)+"\"") { - t.Error("Missing Content for Label 'uid'") - } - if !strings.Contains(labels, "namespace=\""+res.Namespace+"\"") { - t.Error("Missing Content for Label 'namespace'") - } - if !strings.Contains(labels, "version=\""+fixtures.CompleteTargetSendResult.Properties["version"]+"\"") { - t.Error("Missing Content for Label 'version'") - } + assert.Equal(t, res.Kind, stream.Stream["kind"]) + assert.Equal(t, res.Name, stream.Stream["name"]) + assert.Equal(t, string(res.UID), stream.Stream["uid"]) + assert.Equal(t, res.Namespace, stream.Stream["namespace"]) + + assert.Equal(t, fixtures.CompleteTargetSendResult.Properties["version"], stream.Stream["version"]) } client := loki.NewClient(loki.Options{ ClientOptions: target.ClientOptions{ Name: "Loki", }, - Host: "http://localhost:3100/api/prom/push", - CustomLabels: map[string]string{"custom": "label"}, + Host: "http://localhost:3100/loki/api/v1/push", + CustomFields: map[string]string{"custom": "label"}, HTTPClient: testClient{callback, 200}, Username: "username", Password: "password", @@ -119,56 +93,29 @@ func Test_LokiTarget(t *testing.T) { t.Errorf("Unexpected Host: %s", agend) } - if url := req.URL.String(); url != "http://localhost:3100/api/prom/push" { + if url := req.URL.String(); url != "http://localhost:3100/loki/api/v1/push" { t.Errorf("Unexpected Host: %s", url) } - expectedLine := fmt.Sprintf("[%s] %s", strings.ToUpper(fixtures.MinimalTargetSendResult.Priority.String()), fixtures.MinimalTargetSendResult.Message) - labels, line := convertAndValidateBody(req, t) - if line != expectedLine { - t.Errorf("Unexpected LineContent: %s", line) - } - if !strings.Contains(labels, "policy=\""+fixtures.MinimalTargetSendResult.Policy+"\"") { - t.Error("Missing Content for Label 'policy'") - } - if !strings.Contains(labels, "status=\""+string(fixtures.MinimalTargetSendResult.Result)+"\"") { - t.Error("Missing Content for Label 'status'") - } - if !strings.Contains(labels, "priority=\""+fixtures.MinimalTargetSendResult.Priority.String()+"\"") { - t.Error("Missing Content for Label 'priority'") - } - if !strings.Contains(labels, "source=\"policy-reporter\"") { - t.Error("Missing Content for Label 'policy-reporter'") - } - if strings.Contains(labels, "rule") { - t.Error("Unexpected Label 'rule'") - } - if strings.Contains(labels, "category") { - t.Error("Unexpected Label 'category'") - } - if strings.Contains(labels, "severity") { - t.Error("Unexpected 'severity'") - } - if strings.Contains(labels, "kind") { - t.Error("Unexpected Label 'kind'") - } - if strings.Contains(labels, "name") { - t.Error("Unexpected 'name'") - } - if strings.Contains(labels, "uid") { - t.Error("Unexpected 'uid'") - } - if strings.Contains(labels, "namespace") { - t.Error("Unexpected 'namespace'") - } + expectedLine := fmt.Sprintf("[%s] %s", strings.ToUpper(string(fixtures.MinimalTargetSendResult.Severity)), fixtures.MinimalTargetSendResult.Message) + stream := convertAndValidateBody(req, t) + + assert.Equal(t, expectedLine, stream.Values[0][1]) + assert.Equal(t, fixtures.MinimalTargetSendResult.Rule, stream.Stream["rule"]) + assert.Equal(t, fixtures.MinimalTargetSendResult.Policy, stream.Stream["policy"]) + assert.Equal(t, fixtures.MinimalTargetSendResult.Category, stream.Stream["category"]) + assert.Equal(t, string(fixtures.MinimalTargetSendResult.Result), stream.Stream["status"]) + assert.Equal(t, string(fixtures.MinimalTargetSendResult.Severity), stream.Stream["severity"]) + + assert.Equal(t, "policy-reporter", stream.Stream["createdBy"]) } client := loki.NewClient(loki.Options{ ClientOptions: target.ClientOptions{ Name: "Loki", }, - Host: "http://localhost:3100/api/prom/push", - CustomLabels: map[string]string{"custom": "label"}, + Host: "http://localhost:3100/loki/api/v1/push", + CustomFields: map[string]string{"custom": "label"}, HTTPClient: testClient{callback, 200}, }) client.Send(fixtures.MinimalTargetSendResult) @@ -178,8 +125,8 @@ func Test_LokiTarget(t *testing.T) { ClientOptions: target.ClientOptions{ Name: "Loki", }, - Host: "http://localhost:3100/api/prom/push", - CustomLabels: map[string]string{"custom": "label"}, + Host: "http://localhost:3100/loki/api/v1/push", + CustomFields: map[string]string{"custom": "label"}, HTTPClient: testClient{}, }) @@ -189,44 +136,16 @@ func Test_LokiTarget(t *testing.T) { }) } -func convertAndValidateBody(req *http.Request, t *testing.T) (string, string) { - payload := make(map[string]interface{}) +func convertAndValidateBody(req *http.Request, t *testing.T) loki.Stream { + payload := loki.Payload{} err := json.NewDecoder(req.Body).Decode(&payload) if err != nil { t.Fatal(err) } - streamsContent, ok := payload["streams"] - if !ok { - t.Errorf("Expected payload key 'streams' is missing") - } + assert.Len(t, payload.Streams[0].Values, 1) + assert.Len(t, payload.Streams[0].Values[0], 2) - streams := streamsContent.([]interface{}) - if len(streams) != 1 { - t.Errorf("Expected one streams entry") - } - - firstStream := streams[0].(map[string]interface{}) - entriesContent, ok := firstStream["entries"] - if !ok { - t.Errorf("Expected stream key 'entries' is missing") - } - labels, ok := firstStream["labels"] - if !ok { - t.Errorf("Expected stream key 'labels' is missing") - } - - entryContent := entriesContent.([]interface{})[0] - entry := entryContent.(map[string]interface{}) - _, ok = entry["ts"] - if !ok { - t.Errorf("Expected entry key 'ts' is missing") - } - line, ok := entry["line"] - if !ok { - t.Errorf("Expected entry key 'line' is missing") - } - - return labels.(string), line.(string) + return payload.Streams[0] } diff --git a/pkg/helper/aws.go b/pkg/target/provider/aws/aws.go similarity index 97% rename from pkg/helper/aws.go rename to pkg/target/provider/aws/aws.go index e9d3d1fe..3001ed07 100644 --- a/pkg/helper/aws.go +++ b/pkg/target/provider/aws/aws.go @@ -1,4 +1,4 @@ -package helper +package aws import ( "bytes" @@ -21,7 +21,7 @@ import ( var enable = true -type AWSClient interface { +type Client interface { // Upload given Data the configured AWS storage Upload(body *bytes.Buffer, key string) error } @@ -62,7 +62,7 @@ func (s *s3Client) Upload(body *bytes.Buffer, key string) error { } // NewS3Client creates a new S3.client to send Results to S3 -func NewS3Client(accessKeyID, secretAccessKey, region, endpoint, bucket string, pathStyle bool, opts ...Options) AWSClient { +func NewS3Client(accessKeyID, secretAccessKey, region, endpoint, bucket string, pathStyle bool, opts ...Options) Client { config, err := createConfig(accessKeyID, secretAccessKey, region) if err != nil { zap.L().Error("error while creating config", zap.Error(err)) @@ -111,7 +111,7 @@ func (k *kinesisClient) Upload(body *bytes.Buffer, key string) error { } // NewKinesisClient creates a new S3.client to send Results to S3 -func NewKinesisClient(accessKeyID, secretAccessKey, region, endpoint, streamName string) AWSClient { +func NewKinesisClient(accessKeyID, secretAccessKey, region, endpoint, streamName string) Client { config, err := createConfig(accessKeyID, secretAccessKey, region) if err != nil { zap.L().Error("error while creating config", zap.Error(err)) diff --git a/pkg/target/provider/aws/aws_test.go b/pkg/target/provider/aws/aws_test.go new file mode 100644 index 00000000..9f7bdeca --- /dev/null +++ b/pkg/target/provider/aws/aws_test.go @@ -0,0 +1,28 @@ +package aws_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/kyverno/policy-reporter/pkg/helper" + "github.com/kyverno/policy-reporter/pkg/target/provider/aws" +) + +func TestS3Client(t *testing.T) { + client := aws.NewS3Client("access", "secret", "eu-central-1", "http://s3.aws.com", "policy-reporter", false, aws.WithKMS(true, helper.ToPointer("kms"), helper.ToPointer("encryption"))) + + assert.NotNil(t, client) +} + +func TestKinesisClient(t *testing.T) { + client := aws.NewKinesisClient("access", "secret", "eu-central-1", "http://kinesis.aws.com", "policy-reporter") + + assert.NotNil(t, client) +} + +func TestSecurityHubClient(t *testing.T) { + client := aws.NewHubClient("access", "secret", "eu-central-1", "http://securityhub.aws.com") + + assert.NotNil(t, client) +} diff --git a/pkg/helper/gcp.go b/pkg/target/provider/gcs/gcs.go similarity index 72% rename from pkg/helper/gcp.go rename to pkg/target/provider/gcs/gcs.go index 44355eee..96c7a7fa 100644 --- a/pkg/helper/gcp.go +++ b/pkg/target/provider/gcs/gcs.go @@ -1,4 +1,4 @@ -package helper +package gcs import ( "bytes" @@ -10,17 +10,17 @@ import ( "google.golang.org/api/option" ) -type GCPClient interface { +type Client interface { // Upload given Data the configured AWS storage Upload(body *bytes.Buffer, key string) error } -type gcsClient struct { +type client struct { bucket string client *storage.Client } -func (c *gcsClient) Upload(body *bytes.Buffer, key string) error { +func (c *client) Upload(body *bytes.Buffer, key string) error { object := c.client.Bucket(c.bucket).Object(key) writer := object.NewWriter(context.Background()) @@ -34,8 +34,8 @@ func (c *gcsClient) Upload(body *bytes.Buffer, key string) error { return writer.Close() } -// NewGCSClient creates a new GCS.client to send Results to GCS Bucket -func NewGCSClient(ctx context.Context, credentials, bucket string) GCPClient { +// NewClient creates a new GCS.client to send Results to GCS Bucket +func NewClient(ctx context.Context, credentials, bucket string) Client { options := make([]option.ClientOption, 0, 1) if credentials != "" { @@ -48,14 +48,14 @@ func NewGCSClient(ctx context.Context, credentials, bucket string) GCPClient { options = append(options, option.WithCredentials(cred)) } - client, err := storage.NewClient(ctx, options...) + baseClient, err := storage.NewClient(ctx, options...) if err != nil { zap.L().Error("error while creating GCS client", zap.Error(err)) return nil } - return &gcsClient{ + return &client{ bucket, - client, + baseClient, } } diff --git a/pkg/target/s3/s3.go b/pkg/target/s3/s3.go index 45333223..2fafa38c 100644 --- a/pkg/target/s3/s3.go +++ b/pkg/target/s3/s3.go @@ -2,7 +2,6 @@ package s3 import ( "bytes" - "context" "encoding/json" "fmt" "time" @@ -10,23 +9,23 @@ import ( "go.uber.org/zap" "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" - "github.com/kyverno/policy-reporter/pkg/helper" "github.com/kyverno/policy-reporter/pkg/target" "github.com/kyverno/policy-reporter/pkg/target/http" + "github.com/kyverno/policy-reporter/pkg/target/provider/aws" ) // Options to configure the S3 target type Options struct { target.ClientOptions CustomFields map[string]string - S3 helper.AWSClient + S3 aws.Client Prefix string } type client struct { target.BaseClient customFields map[string]string - s3 helper.AWSClient + s3 aws.Client prefix string } @@ -54,8 +53,7 @@ func (c *client) Send(result v1alpha2.PolicyReportResult) { t := time.Unix(result.Timestamp.Seconds, int64(result.Timestamp.Nanos)) key := fmt.Sprintf("%s/%s/%s-%s-%s.json", c.prefix, t.Format("2006-01-02"), result.Policy, result.ID, t.Format(time.RFC3339Nano)) - err := c.s3.Upload(body, key) - if err != nil { + if err := c.s3.Upload(body, key); err != nil { zap.L().Error(c.Name()+": S3 Upload error", zap.Error(err)) return } @@ -63,7 +61,9 @@ func (c *client) Send(result v1alpha2.PolicyReportResult) { zap.L().Info(c.Name() + ": PUSH OK") } -func (c *client) CleanUp(_ context.Context, _ v1alpha2.ReportInterface) {} +func (c *client) Type() target.ClientType { + return target.SingleSend +} // NewClient creates a new S3.client to send Results to S3. func NewClient(options Options) target.Client { diff --git a/pkg/target/securityhub/securityhub.go b/pkg/target/securityhub/securityhub.go index cf763675..448e4c77 100644 --- a/pkg/target/securityhub/securityhub.go +++ b/pkg/target/securityhub/securityhub.go @@ -3,6 +3,7 @@ package securityhub import ( "context" "fmt" + "strings" "time" hub "github.com/aws/aws-sdk-go-v2/service/securityhub" @@ -10,12 +11,20 @@ import ( "go.uber.org/zap" "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" + "github.com/kyverno/policy-reporter/pkg/helper" "github.com/kyverno/policy-reporter/pkg/target" ) +var schema = toPointer("2018-10-08") + type HubClient interface { BatchImportFindings(ctx context.Context, params *hub.BatchImportFindingsInput, optFns ...func(*hub.Options)) (*hub.BatchImportFindingsOutput, error) GetFindings(ctx context.Context, params *hub.GetFindingsInput, optFns ...func(*hub.Options)) (*hub.GetFindingsOutput, error) + BatchUpdateFindings(ctx context.Context, params *hub.BatchUpdateFindingsInput, optFns ...func(*hub.Options)) (*hub.BatchUpdateFindingsOutput, error) +} + +type PolrClient interface { + Get(ctx context.Context, name, namespace string) (v1alpha2.ReportInterface, error) } // Options to configure the SecurityHub target @@ -28,7 +37,7 @@ type Options struct { ProductName string CompanyName string Delay time.Duration - Cleanup bool + Synchronize bool } type client struct { @@ -40,116 +49,205 @@ type client struct { productName string companyName string delay time.Duration - cleanup bool + synchronize bool + arn *string } -func (c *client) Send(result v1alpha2.PolicyReportResult) { - generator := result.Policy - if generator == "" { - generator = result.Rule - } - - title := generator - if result.HasResource() { - title = fmt.Sprintf("%s: %s", title, result.ResourceString()) - } - - t := time.Unix(result.Timestamp.Seconds, int64(result.Timestamp.Nanos)) - +func (c *client) mapFindings(polr v1alpha2.ReportInterface, results []v1alpha2.PolicyReportResult) []types.AwsSecurityFinding { var accID *string if c.accountID != "" { accID = toPointer(c.accountID) } - res, err := c.hub.BatchImportFindings(context.TODO(), &hub.BatchImportFindingsInput{ - Findings: []types.AwsSecurityFinding{ - { - Id: toPointer(result.GetID()), - AwsAccountId: accID, - SchemaVersion: toPointer("2018-10-08"), - ProductArn: toPointer("arn:aws:securityhub:" + c.region + ":" + c.accountID + ":product/" + c.accountID + "/default"), - GeneratorId: toPointer(fmt.Sprintf("%s/%s", result.Source, generator)), - Types: []string{mapType(result.Source)}, - CreatedAt: toPointer(t.Format("2006-01-02T15:04:05.999999999Z07:00")), - UpdatedAt: toPointer(t.Format("2006-01-02T15:04:05.999999999Z07:00")), - Severity: &types.Severity{ - Label: MapSeverity(result.Severity), - }, - Title: &title, - Description: &result.Message, - ProductName: &c.productName, - CompanyName: &c.companyName, - Compliance: &types.Compliance{ - Status: types.ComplianceStatusFailed, - }, - Resources: []types.Resource{ - { - Type: toPointer("Other"), - Region: &c.region, - Partition: types.PartitionAws, - Id: mapResourceID(result), - Details: &types.ResourceDetails{ - Other: c.mapOtherDetails(result), - }, + return helper.Map(results, func(result v1alpha2.PolicyReportResult) types.AwsSecurityFinding { + generator := result.Policy + if generator == "" { + generator = result.Rule + } + + title := generator + if result.HasResource() { + title = fmt.Sprintf("%s: %s", title, result.ResourceString()) + } + + t := time.Unix(result.Timestamp.Seconds, int64(result.Timestamp.Nanos)) + + return types.AwsSecurityFinding{ + Id: toPointer(result.GetID()), + AwsAccountId: accID, + SchemaVersion: schema, + ProductArn: c.arn, + GeneratorId: toPointer(fmt.Sprintf("%s/%s", result.Source, generator)), + Types: []string{mapType(result.Source)}, + CreatedAt: toPointer(t.Format("2006-01-02T15:04:05.999999999Z07:00")), + UpdatedAt: toPointer(t.Format("2006-01-02T15:04:05.999999999Z07:00")), + Severity: &types.Severity{ + Label: MapSeverity(result.Severity), + }, + Title: &title, + Description: &result.Message, + ProductName: &c.productName, + CompanyName: &c.companyName, + Compliance: &types.Compliance{ + Status: types.ComplianceStatusFailed, + }, + Workflow: &types.Workflow{ + Status: types.WorkflowStatusNew, + }, + Resources: []types.Resource{ + { + Type: toPointer("Other"), + Region: &c.region, + Partition: types.PartitionAws, + Id: mapResourceID(result), + Details: &types.ResourceDetails{ + Other: c.mapOtherDetails(polr, result), }, }, - RecordState: types.RecordStateActive, }, - }, + RecordState: types.RecordStateActive, + } + }) +} + +func (c *client) Send(result v1alpha2.PolicyReportResult) { + c.BatchSend(&v1alpha2.PolicyReport{}, []v1alpha2.PolicyReportResult{result}) +} + +func filterResults(results []v1alpha2.PolicyReportResult) []v1alpha2.PolicyReportResult { + return helper.Filter(results, func(r v1alpha2.PolicyReportResult) bool { + if r.Result == v1alpha2.StatusFail { + return true + } + if r.Result == v1alpha2.StatusWarn { + return true + } + if r.Result == v1alpha2.StatusError { + return true + } + + return false + }) +} + +func (c *client) BatchSend(polr v1alpha2.ReportInterface, results []v1alpha2.PolicyReportResult) { + results = filterResults(results) + if len(results) == 0 { + return + } + + list, err := c.getFindingsByIDs(context.Background(), polr, toResourceIDFilter(polr, results), "") + if err != nil { + zap.L().Error(c.Name()+": failed to get findings", zap.Error(err)) + return + } + + list = filterFindings(list, results) + findings := helper.Map(list, func(f types.AwsSecurityFinding) types.AwsSecurityFindingIdentifier { + return types.AwsSecurityFindingIdentifier{ + Id: f.Id, + ProductArn: f.ProductArn, + } + }) + + if len(findings) > 0 { + updated, err := c.batchUpdate(context.Background(), findings, types.WorkflowStatusNew) + if err != nil { + zap.L().Error(c.Name()+": PUSH FAILED", zap.Error(err)) + return + } else if updated > 0 { + zap.L().Info(c.Name()+": PUSH OK", zap.Int("updated", updated)) + } + + mapping := make(map[string]bool, len(list)) + for _, f := range list { + mapping[*f.Id] = true + } + + results = helper.Filter(results, func(result v1alpha2.PolicyReportResult) bool { + return !mapping[result.GetID()] + }) + } + + if len(results) == 0 { + return + } + + res, err := c.hub.BatchImportFindings(context.Background(), &hub.BatchImportFindingsInput{ + Findings: c.mapFindings(polr, results), }) if err != nil { zap.L().Error(c.Name()+": PUSH FAILED", zap.Error(err), zap.Any("response", res)) return } - zap.L().Info(c.Name()+": PUSH OK", zap.Int32("successCount", *res.SuccessCount), zap.Int32("failedCount", *res.FailedCount)) + zap.L().Info(c.Name()+": PUSH OK", zap.Int32("imported", *res.SuccessCount), zap.Int32("failed", *res.FailedCount), zap.String("report", polr.GetKey())) +} + +func (c *client) Reset(ctx context.Context) error { + if !c.synchronize { + return nil + } + + zap.L().Info(c.Name() + ": START SYNC") + + list, err := c.getFindings(ctx) + if err != nil { + zap.L().Error(c.Name()+": failed to get findings", zap.Error(err)) + return err + } + + if len(list) == 0 { + zap.L().Info(c.Name() + ": no findings to sync") + return nil + } + + findings := helper.Map(list, func(f types.AwsSecurityFinding) types.AwsSecurityFindingIdentifier { + return types.AwsSecurityFindingIdentifier{ + Id: f.Id, + ProductArn: f.ProductArn, + } + }) + + count, err := c.batchUpdate(ctx, findings, types.WorkflowStatusResolved) + if err != nil { + zap.L().Error(c.Name()+": failed to sync findings", zap.Error(err)) + return err + } + + zap.L().Info(c.Name()+": FINISHED SYNC", zap.Int("updated", count)) + + return nil } func (c *client) CleanUp(ctx context.Context, report v1alpha2.ReportInterface) { - if !c.cleanup { + if !c.synchronize { return } - resourceIds := toResourceIDFilter(report) - if len(resourceIds) == 0 { - return + zap.L().Info(c.Name()+": start cleanup", zap.String("report", report.GetKey())) + + if report.GetSource() != "" { + if !c.BaseClient.ValidateReport(report) { + return + } } - findings, err := c.hub.GetFindings(ctx, &hub.GetFindingsInput{ - Filters: &types.AwsSecurityFindingFilters{ - Region: []types.StringFilter{ - { - Comparison: types.StringFilterComparisonEquals, - Value: &c.region, - }, - }, - Type: []types.StringFilter{ - { - Comparison: types.StringFilterComparisonPrefix, - Value: toPointer(mapType(report.GetSource())), - }, - }, - ResourceId: resourceIds, - RecordState: []types.StringFilter{ - { - Comparison: types.StringFilterComparisonEquals, - Value: toPointer("ACTIVE"), - }, - }, - }, - }) + resourceIds := toResourceIDFilter(report, report.GetResults()) + + findings, err := c.getFindingsByIDs(ctx, report, resourceIds, "") if err != nil { zap.L().Error(c.Name()+": failed to get findings", zap.Error(err)) return } + defer time.Sleep(c.delay) - if len(findings.Findings) == 0 { - time.Sleep(c.delay) + if len(findings) == 0 { return } - mapping := make(map[string]types.AwsSecurityFinding, len(findings.Findings)) - for _, f := range findings.Findings { + mapping := make(map[string]types.AwsSecurityFinding, len(findings)) + for _, f := range findings { mapping[*f.Id] = f } @@ -162,39 +260,34 @@ func (c *client) CleanUp(ctx context.Context, report v1alpha2.ReportInterface) { } if len(mapping) == 0 { - time.Sleep(c.delay) return } - list := make([]types.AwsSecurityFinding, 0) + list := make([]types.AwsSecurityFindingIdentifier, 0, len(mapping)) for _, f := range mapping { - f.UpdatedAt = toPointer(time.Now().Format("2006-01-02T15:04:05.999999999Z07:00")) - f.RecordState = types.RecordStateArchived - f.Workflow = &types.Workflow{ - Status: types.WorkflowStatusResolved, - } - - list = append(list, f) + list = append(list, types.AwsSecurityFindingIdentifier{ + Id: f.Id, + ProductArn: f.ProductArn, + }) } - if _, err = c.hub.BatchImportFindings(ctx, &hub.BatchImportFindingsInput{Findings: list}); err != nil { - zap.L().Error(c.Name()+": failed to batch archived findings", zap.Error(err)) - time.Sleep(c.delay) + count, err := c.batchUpdate(ctx, list, types.WorkflowStatusResolved) + if err != nil { + zap.L().Error(c.Name()+": failed to batch resolve findings", zap.Error(err)) return } - zap.L().Info(c.Name()+": Findings updated", zap.Int("count", len(list))) - time.Sleep(c.delay) + zap.L().Info(c.Name()+": CLEANUP OK", zap.Int("count", count), zap.String("report", report.GetKey())) } -func (c *client) mapOtherDetails(result v1alpha2.PolicyReportResult) map[string]string { +func (c *client) mapOtherDetails(polr v1alpha2.ReportInterface, result v1alpha2.PolicyReportResult) map[string]string { details := map[string]string{ "Source": result.Source, "Category": result.Category, "Policy": result.Policy, "Rule": result.Rule, "Result": string(result.Result), - "Priority": result.Priority.String(), + "Report": polr.GetKey(), } if len(c.customFields) > 0 { @@ -230,8 +323,177 @@ func (c *client) mapOtherDetails(result v1alpha2.PolicyReportResult) map[string] return details } +func (c *client) getFindings(ctx context.Context) ([]types.AwsSecurityFinding, error) { + list := make([]types.AwsSecurityFinding, 0) + + var token *string + + for { + resp, err := c.hub.GetFindings(ctx, &hub.GetFindingsInput{ + NextToken: token, + Filters: c.BaseFilter(nil), + }) + if err != nil { + return nil, err + } + + if len(resp.Findings) == 0 { + return list, nil + } + + list = append(list, resp.Findings...) + if resp.NextToken == nil { + return list, nil + } + + token = resp.NextToken + } +} + +func (c *client) batchUpdate(ctx context.Context, findings []types.AwsSecurityFindingIdentifier, status types.WorkflowStatus) (int, error) { + if len(findings) == 0 { + return 0, nil + } + + chunks := helper.ChunkSlice(findings, 100) + + var updated int + for _, chunk := range chunks { + response, err := c.hub.BatchUpdateFindings(ctx, &hub.BatchUpdateFindingsInput{ + FindingIdentifiers: chunk, + Workflow: &types.WorkflowUpdate{ + Status: status, + }, + }) + if err != nil { + return updated, err + } + + updated += len(response.ProcessedFindings) + } + + return updated, nil +} + +func (c *client) getFindingsByIDs(ctx context.Context, report v1alpha2.ReportInterface, resources []types.StringFilter, status string) ([]types.AwsSecurityFinding, error) { + list := make([]types.AwsSecurityFinding, 0) + + chunks := helper.ChunkSlice(resources, 20) + + for _, res := range chunks { + filter := c.BaseFilter(report) + if len(res) > 0 { + filter.ResourceId = res + } + + if status != "" { + filter.WorkflowStatus = []types.StringFilter{ + { + Comparison: types.StringFilterComparisonEquals, + Value: toPointer(status), + }, + } + } + + var token *string + + for { + resp, err := c.hub.GetFindings(ctx, &hub.GetFindingsInput{ + NextToken: token, + Filters: filter, + }) + if err != nil { + return nil, err + } + + if len(resp.Findings) == 0 { + break + } + + list = append(list, resp.Findings...) + if resp.NextToken == nil { + break + } + + token = resp.NextToken + } + } + + return list, nil +} + +func (c *client) BaseFilter(report v1alpha2.ReportInterface) *types.AwsSecurityFindingFilters { + source := "" + if report != nil { + source = report.GetSource() + } + + filter := &types.AwsSecurityFindingFilters{ + ProductArn: []types.StringFilter{ + { + Comparison: types.StringFilterComparisonEquals, + Value: c.arn, + }, + }, + AwsAccountId: []types.StringFilter{ + { + Comparison: types.StringFilterComparisonEquals, + Value: &c.accountID, + }, + }, + Region: []types.StringFilter{ + { + Comparison: types.StringFilterComparisonEquals, + Value: &c.region, + }, + }, + Type: []types.StringFilter{ + { + Comparison: types.StringFilterComparisonPrefix, + Value: toPointer(mapType(source)), + }, + }, + ProductName: []types.StringFilter{ + { + Comparison: types.StringFilterComparisonEquals, + Value: &c.productName, + }, + }, + RecordState: []types.StringFilter{ + { + Comparison: types.StringFilterComparisonEquals, + Value: toPointer("ACTIVE"), + }, + }, + } + + if report != nil { + filter.ResourceDetailsOther = []types.MapFilter{ + { + Comparison: types.MapFilterComparisonEquals, + Key: toPointer("Report"), + Value: toPointer(report.GetKey()), + }, + } + } + + return filter +} + +func (c *client) Type() target.ClientType { + if !c.synchronize { + return target.BatchSend + } + + return target.SyncSend +} + // NewClient creates a new SecurityHub.client to send Results to SecurityHub. -func NewClient(options Options) target.Client { +func NewClient(options Options) *client { + if options.Delay == 0 { + options.Delay = 2 * time.Second + } + return &client{ target.NewBaseClient(options.ClientOptions), options.CustomFields, @@ -241,7 +503,8 @@ func NewClient(options Options) target.Client { options.ProductName, options.CompanyName, options.Delay, - options.Cleanup, + options.Synchronize, + toPointer("arn:aws:securityhub:" + options.Region + ":" + options.AccountID + ":product/" + options.AccountID + "/default"), } } @@ -287,7 +550,7 @@ func mapType(source string) string { return "Software and Configuration Checks/Kubernetes Policies/" + source } -func toResourceIDFilter(report v1alpha2.ReportInterface) []types.StringFilter { +func toResourceIDFilter(report v1alpha2.ReportInterface, results []v1alpha2.PolicyReportResult) []types.StringFilter { res := report.GetScope() if res != nil { var value string @@ -305,7 +568,7 @@ func toResourceIDFilter(report v1alpha2.ReportInterface) []types.StringFilter { } } - if len(report.GetResults()) == 0 { + if len(results) == 0 { return []types.StringFilter{ { Comparison: types.StringFilterComparisonEquals, @@ -315,7 +578,7 @@ func toResourceIDFilter(report v1alpha2.ReportInterface) []types.StringFilter { } list := map[string]bool{} - for _, result := range report.GetResults() { + for _, result := range results { list[*mapResourceID(result)] = true } @@ -329,3 +592,29 @@ func toResourceIDFilter(report v1alpha2.ReportInterface) []types.StringFilter { return filter } + +func splitPolrKey(key string) (string, string) { + parts := strings.Split(key, "/") + if len(parts) == 1 { + return parts[0], "" + } + + return parts[1], parts[0] +} + +func filterFindings(findings []types.AwsSecurityFinding, results []v1alpha2.PolicyReportResult) []types.AwsSecurityFinding { + filtered := make([]types.AwsSecurityFinding, 0, len(findings)) + + mapping := make(map[string]bool, len(results)) + for _, r := range results { + mapping[r.GetID()] = true + } + + for _, finding := range findings { + if _, ok := mapping[*finding.Id]; ok { + filtered = append(filtered, finding) + } + } + + return filtered +} diff --git a/pkg/target/securityhub/securityhub_test.go b/pkg/target/securityhub/securityhub_test.go index 211af6ee..5ea020bd 100644 --- a/pkg/target/securityhub/securityhub_test.go +++ b/pkg/target/securityhub/securityhub_test.go @@ -7,6 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" hub "github.com/aws/aws-sdk-go-v2/service/securityhub" "github.com/aws/aws-sdk-go-v2/service/securityhub/types" + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" "github.com/kyverno/policy-reporter/pkg/fixtures" "github.com/kyverno/policy-reporter/pkg/target/securityhub" @@ -40,10 +41,16 @@ func (c *client) GetFindings(ctx context.Context, params *hub.GetFindingsInput, }, nil } +func (c *client) BatchUpdateFindings(ctx context.Context, params *hub.BatchUpdateFindingsInput, optFns ...func(*hub.Options)) (*hub.BatchUpdateFindingsOutput, error) { + c.batched = true + + return &hub.BatchUpdateFindingsOutput{}, nil +} + func TestSecurityHub(t *testing.T) { t.Run("send result", func(t *testing.T) { c := securityhub.NewClient(securityhub.Options{ - AccountID: "accountID", + AccountID: "accountId", Region: "eu-central-1", ProductName: "Policy Reporter", CompanyName: "Kyverno", @@ -56,13 +63,13 @@ func TestSecurityHub(t *testing.T) { finding := findings[0] - if *finding.AwsAccountId != "accountID" { - t.Errorf("unexpected accountID: %s", *finding.AwsAccountId) + if *finding.AwsAccountId != "accountId" { + t.Errorf("unexpected accountId: %s", *finding.AwsAccountId) } if *finding.Id != fixtures.CompleteTargetSendResult.GetID() { t.Errorf("unexpected id: %s", *finding.Id) } - if *finding.ProductArn != "arn:aws:securityhub:eu-central-1:accountID:product/accountID/default" { + if *finding.ProductArn != "arn:aws:securityhub:eu-central-1:accountId:product/accountId/default" { t.Errorf("unexpected product arn: %s", *finding.ProductArn) } if *finding.ProductName != "Policy Reporter" { @@ -81,12 +88,12 @@ func TestSecurityHub(t *testing.T) { h := &client{} c := securityhub.NewClient(securityhub.Options{ - AccountID: "accountID", + AccountID: "accountId", Region: "eu-central-1", ProductName: "Policy Reporter", CompanyName: "Kyverno", Client: h, - Cleanup: false, + Synchronize: false, }) c.CleanUp(context.TODO(), fixtures.DefaultPolicyReport) @@ -102,12 +109,12 @@ func TestSecurityHub(t *testing.T) { h := &client{} c := securityhub.NewClient(securityhub.Options{ - AccountID: "accountID", + AccountID: "accountId", Region: "eu-central-1", ProductName: "Policy Reporter", CompanyName: "Kyverno", Client: h, - Cleanup: true, + Synchronize: true, }) c.CleanUp(context.TODO(), fixtures.DefaultPolicyReport) @@ -129,12 +136,12 @@ func TestSecurityHub(t *testing.T) { } c := securityhub.NewClient(securityhub.Options{ - AccountID: "accountID", + AccountID: "accountId", Region: "eu-central-1", ProductName: "Policy Reporter", CompanyName: "Kyverno", Client: h, - Cleanup: true, + Synchronize: true, }) c.CleanUp(context.TODO(), fixtures.DefaultPolicyReport) @@ -156,12 +163,12 @@ func TestSecurityHub(t *testing.T) { } c := securityhub.NewClient(securityhub.Options{ - AccountID: "accountID", + AccountID: "accountId", Region: "eu-central-1", ProductName: "Policy Reporter", CompanyName: "Kyverno", Client: h, - Cleanup: true, + Synchronize: true, }) c.CleanUp(context.TODO(), fixtures.DefaultPolicyReport) diff --git a/pkg/target/slack/slack.go b/pkg/target/slack/slack.go index e69a907f..35b33449 100644 --- a/pkg/target/slack/slack.go +++ b/pkg/target/slack/slack.go @@ -1,192 +1,169 @@ package slack import ( - "context" - "strings" + "fmt" + + "github.com/slack-go/slack" + "go.uber.org/zap" "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" + "github.com/kyverno/policy-reporter/pkg/helper" "github.com/kyverno/policy-reporter/pkg/target" + "github.com/kyverno/policy-reporter/pkg/target/formatting" "github.com/kyverno/policy-reporter/pkg/target/http" ) // Options to configure the Slack target type Options struct { target.ClientOptions - Webhook string Channel string + Webhook string CustomFields map[string]string + Headers map[string]string HTTPClient http.Client } -type text struct { - Type string `json:"type"` - Text string `json:"text"` -} - -type block struct { - Type string `json:"type"` - Text *text `json:"text,omitempty"` - Fields []field `json:"fields,omitempty"` -} - -type field struct { - Type string `json:"type"` - Text string `json:"text"` -} - -type attachment struct { - Color string `json:"color"` - Blocks []block `json:"blocks"` -} - -type payload struct { - Channel string `json:"channel,omitempty"` - Username string `json:"username,omitempty"` - Attachments []attachment `json:"attachments,omitempty"` -} - type client struct { target.BaseClient - webhook string channel string + webhook string client http.Client customFields map[string]string + headers map[string]string } -var colors = map[v1alpha2.Priority]string{ - v1alpha2.DebugPriority: "#68c2ff", - v1alpha2.InfoPriority: "#36a64f", - v1alpha2.WarningPriority: "#f2c744", - v1alpha2.CriticalPriority: "#b80707", - v1alpha2.ErrorPriority: "#e20b0b", +var colors = map[v1alpha2.PolicySeverity]string{ + v1alpha2.SeverityInfo: "#68c2ff", + v1alpha2.SeverityLow: "#36a64f", + v1alpha2.SeverityMedium: "#f2c744", + v1alpha2.SeverityHigh: "#b80707", + v1alpha2.SeverityCritical: "#e20b0b", } -func (s *client) newPayload(result v1alpha2.PolicyReportResult) payload { - p := payload{ - Attachments: make([]attachment, 0, 1), +func (s *client) message(result v1alpha2.PolicyReportResult) *slack.WebhookMessage { + p := &slack.WebhookMessage{ + Attachments: make([]slack.Attachment, 0, 1), + Channel: s.channel, } if s.channel != "" { p.Channel = s.channel } - att := attachment{ - Color: colors[result.Priority], - Blocks: make([]block, 0), + att := slack.Attachment{ + Color: colors[result.Severity], + Blocks: slack.Blocks{ + BlockSet: make([]slack.Block, 0), + }, } - policyBlock := block{ - Type: "section", - Fields: []field{{Type: "mrkdwn", Text: "*Policy*\n" + result.Policy}}, - } + policyBlock := slack.NewSectionBlock(nil, []*slack.TextBlockObject{slack.NewTextBlockObject(slack.MarkdownType, "*Policy*\n"+result.Policy, false, false)}, nil) if result.Rule != "" { - policyBlock.Fields = append(policyBlock.Fields, field{Type: "mrkdwn", Text: "*Rule*\n" + result.Rule}) + policyBlock.Fields = append(policyBlock.Fields, slack.NewTextBlockObject(slack.MarkdownType, "*Rule*\n"+result.Rule, false, false)) } - att.Blocks = append( - att.Blocks, - block{Type: "header", Text: &text{Type: "plain_text", Text: "New Policy Report Result"}}, + att.Blocks.BlockSet = append( + att.Blocks.BlockSet, + slack.NewHeaderBlock(slack.NewTextBlockObject(slack.PlainTextType, "New Policy Report Result", false, false)), policyBlock, ) - att.Blocks = append( - att.Blocks, - block{Type: "section", Text: &text{Type: "mrkdwn", Text: "*Message*\n" + result.Message}}, - block{ - Type: "section", - Fields: []field{ - {Type: "mrkdwn", Text: "*Priority*\n" + result.Priority.String()}, - {Type: "mrkdwn", Text: "*Status*\n" + string(result.Result)}, - }, - }, + att.Blocks.BlockSet = append( + att.Blocks.BlockSet, + slack.NewSectionBlock(slack.NewTextBlockObject(slack.MarkdownType, "*Message*\n"+result.Message, false, false), nil, nil), + slack.NewSectionBlock(nil, []*slack.TextBlockObject{ + slack.NewTextBlockObject(slack.MarkdownType, "*Status*\n"+string(result.Result), false, false), + }, nil), ) - b := block{ - Type: "section", - Fields: make([]field, 0, 2), - } + b := slack.NewSectionBlock(nil, make([]*slack.TextBlockObject, 0, 2), nil) if result.Category != "" { - b.Fields = append(b.Fields, field{Type: "mrkdwn", Text: "*Category*\n" + result.Category}) + b.Fields = append(b.Fields, slack.NewTextBlockObject(slack.MarkdownType, "*Category*\n"+result.Category, false, false)) } if result.Severity != "" { - b.Fields = append(b.Fields, field{Type: "mrkdwn", Text: "*Severity*\n" + string(result.Severity)}) + b.Fields = append(b.Fields, slack.NewTextBlockObject(slack.MarkdownType, "*Severity*\n"+string(result.Severity), false, false)) } if len(b.Fields) > 0 { - att.Blocks = append(att.Blocks, b) + att.Blocks.BlockSet = append(att.Blocks.BlockSet, b) } if result.HasResource() { res := result.GetResource() - att.Blocks = append(att.Blocks, block{Type: "section", Text: &text{Type: "mrkdwn", Text: "*Resource*"}}) + att.Blocks.BlockSet = append( + att.Blocks.BlockSet, + slack.NewSectionBlock(slack.NewTextBlockObject(slack.MarkdownType, "*Resource*", false, false), nil, nil), + ) if res.APIVersion != "" { - att.Blocks = append(att.Blocks, block{ - Type: "section", - Fields: []field{ - {Type: "mrkdwn", Text: "*Kind*\n" + res.Kind}, - {Type: "mrkdwn", Text: "*API Version*\n" + res.APIVersion}, - }, - }) + att.Blocks.BlockSet = append( + att.Blocks.BlockSet, + slack.NewSectionBlock(nil, []*slack.TextBlockObject{ + slack.NewTextBlockObject(slack.MarkdownType, "*Kind*\n"+res.Kind, false, false), + slack.NewTextBlockObject(slack.MarkdownType, "*API Version*\n"+res.APIVersion, false, false), + }, nil), + ) } else if res.APIVersion == "" && res.UID != "" { - att.Blocks = append(att.Blocks, block{ - Type: "section", - Text: &text{Type: "mrkdwn", Text: "*Kind*\n" + res.Kind}, - }) + att.Blocks.BlockSet = append( + att.Blocks.BlockSet, + slack.NewSectionBlock(nil, []*slack.TextBlockObject{ + slack.NewTextBlockObject(slack.MarkdownType, "*Kind*\n"+res.Kind, false, false), + }, nil), + ) } if res.UID != "" { - att.Blocks = append(att.Blocks, block{ - Type: "section", - Fields: []field{ - {Type: "mrkdwn", Text: "*Name*\n" + res.Name}, - {Type: "mrkdwn", Text: "*UID*\n" + string(res.UID)}, - }, - }) + att.Blocks.BlockSet = append( + att.Blocks.BlockSet, + slack.NewSectionBlock(nil, []*slack.TextBlockObject{ + slack.NewTextBlockObject(slack.MarkdownType, "*Name*\n"+res.Name, false, false), + slack.NewTextBlockObject(slack.MarkdownType, "*UID*\n"+string(res.UID), false, false), + }, nil), + ) } else if res.UID == "" && res.APIVersion != "" { - att.Blocks = append(att.Blocks, block{ - Type: "section", - Text: &text{Type: "mrkdwn", Text: "*Name*\n" + res.Name}, - }) + att.Blocks.BlockSet = append( + att.Blocks.BlockSet, + slack.NewSectionBlock(nil, []*slack.TextBlockObject{slack.NewTextBlockObject(slack.MarkdownType, "*Name*\n"+res.Name, false, false)}, nil), + ) } if res.APIVersion == "" && res.UID == "" { - att.Blocks = append(att.Blocks, block{ - Type: "section", - Fields: []field{ - {Type: "mrkdwn", Text: "*Kind*\n" + res.Kind}, - {Type: "mrkdwn", Text: "*Name*\n" + res.Name}, - }, - }) + att.Blocks.BlockSet = append( + att.Blocks.BlockSet, + slack.NewSectionBlock(nil, []*slack.TextBlockObject{ + slack.NewTextBlockObject(slack.MarkdownType, "*Kind*\n"+res.Kind, false, false), + slack.NewTextBlockObject(slack.MarkdownType, "*Name*\n"+res.Name, false, false), + }, nil), + ) } if res.Namespace != "" { - att.Blocks = append(att.Blocks, block{Type: "section", Fields: []field{{Type: "mrkdwn", Text: "*Namespace*\n" + res.Namespace}}}) + att.Blocks.BlockSet = append( + att.Blocks.BlockSet, + slack.NewSectionBlock(nil, []*slack.TextBlockObject{slack.NewTextBlockObject(slack.MarkdownType, "*Namespace*\n"+res.Namespace, false, false)}, nil), + ) } } if len(result.Properties) > 0 || len(s.customFields) > 0 { - att.Blocks = append( - att.Blocks, - block{Type: "section", Text: &text{Type: "mrkdwn", Text: "*Properties*"}}, + att.Blocks.BlockSet = append( + att.Blocks.BlockSet, + slack.NewSectionBlock(slack.NewTextBlockObject(slack.MarkdownType, "*Properties*", false, false), nil, nil), ) - propBlock := block{ - Type: "section", - Fields: []field{}, - } + propBlock := slack.NewSectionBlock(nil, make([]*slack.TextBlockObject, 0), nil) for property, value := range result.Properties { - propBlock.Fields = append(propBlock.Fields, field{Type: "mrkdwn", Text: "*" + strings.Title(property) + "*\n" + value}) + propBlock.Fields = append(propBlock.Fields, slack.NewTextBlockObject(slack.MarkdownType, "*"+helper.Title(property)+"*\n"+value, false, false)) } for property, value := range s.customFields { - propBlock.Fields = append(propBlock.Fields, field{Type: "mrkdwn", Text: "*" + strings.Title(property) + "*\n" + value}) + propBlock.Fields = append(propBlock.Fields, slack.NewTextBlockObject(slack.MarkdownType, "*"+helper.Title(property)+"*\n"+value, false, false)) } - att.Blocks = append(att.Blocks, propBlock) + att.Blocks.BlockSet = append(att.Blocks.BlockSet, propBlock) } p.Attachments = append(p.Attachments, att) @@ -194,25 +171,155 @@ func (s *client) newPayload(result v1alpha2.PolicyReportResult) payload { return p } +func (s *client) batchMessage(polr v1alpha2.ReportInterface, results []v1alpha2.PolicyReportResult) *slack.WebhookMessage { + scope := polr.GetScope() + resource := formatting.ResourceString(scope) + + p := &slack.WebhookMessage{ + Attachments: make([]slack.Attachment, 0, 1), + Channel: s.channel, + } + + if s.channel != "" { + p.Channel = s.channel + } + + att := slack.Attachment{ + Color: colors[v1alpha2.SeverityInfo], + Blocks: slack.Blocks{ + BlockSet: make([]slack.Block, 0), + }, + } + + att.Blocks.BlockSet = append( + att.Blocks.BlockSet, + slack.NewHeaderBlock(slack.NewTextBlockObject(slack.PlainTextType, resource+" Policy Report Result", false, false)), + slack.NewSectionBlock(slack.NewTextBlockObject(slack.MarkdownType, fmt.Sprintf("Received %d new Policy Report Results", len(results)), false, false), nil, nil), + ) + + cfl := len(s.customFields) + if cfl > 0 { + att.Blocks.BlockSet = append(att.Blocks.BlockSet, slack.NewDividerBlock(), slack.NewSectionBlock(slack.NewTextBlockObject(slack.MarkdownType, "*Custom Fields*", false, false), nil, nil)) + + i := 0 + + var propBlock *slack.SectionBlock + for property, value := range s.customFields { + if i%2 == 0 { + propBlock = slack.NewSectionBlock(nil, make([]*slack.TextBlockObject, 0, 2), nil) + att.Blocks.BlockSet = append(att.Blocks.BlockSet, propBlock) + } + + propBlock.Fields = append(propBlock.Fields, slack.NewTextBlockObject(slack.MarkdownType, "*"+helper.Title(property)+"*\n"+value, false, false)) + i++ + } + } + + p.Attachments = append(p.Attachments, att) + + for _, result := range results { + resultAttachment := slack.Attachment{ + Color: colors[result.Severity], + Blocks: slack.Blocks{ + BlockSet: make([]slack.Block, 0), + }, + } + + policy := fmt.Sprintf("Policy: %s", result.Policy) + + if result.Rule != "" { + policy = fmt.Sprintf("%s/%s", policy, result.Rule) + } + + resultAttachment.Blocks.BlockSet = append( + resultAttachment.Blocks.BlockSet, + slack.NewHeaderBlock(slack.NewTextBlockObject(slack.PlainTextType, policy, false, false)), + ) + + if result.Category != "" { + resultAttachment.Blocks.BlockSet = append( + resultAttachment.Blocks.BlockSet, + slack.NewContextBlock("", slack.NewTextBlockObject(slack.MarkdownType, "*"+result.Category+"*", false, false)), + ) + } + + b := slack.NewSectionBlock(nil, []*slack.TextBlockObject{ + slack.NewTextBlockObject(slack.MarkdownType, "*Status*\n"+string(result.Result), false, false), + }, nil) + + if result.Severity != "" { + b.Fields = append(b.Fields, slack.NewTextBlockObject(slack.MarkdownType, "*Severity*\n"+string(result.Severity), false, false)) + } + + resultAttachment.Blocks.BlockSet = append(resultAttachment.Blocks.BlockSet, b) + + resultAttachment.Blocks.BlockSet = append( + resultAttachment.Blocks.BlockSet, + slack.NewSectionBlock(slack.NewTextBlockObject(slack.MarkdownType, "*Message*\n"+result.Message, false, false), nil, nil), + ) + + if len(result.Properties) > 0 { + resultAttachment.Blocks.BlockSet = append(resultAttachment.Blocks.BlockSet, slack.NewSectionBlock(slack.NewTextBlockObject(slack.MarkdownType, "*Properties*", false, false), nil, nil)) + + propBlock := slack.NewSectionBlock(nil, make([]*slack.TextBlockObject, 0), nil) + + for property, value := range result.Properties { + propBlock.Fields = append(propBlock.Fields, slack.NewTextBlockObject(slack.MarkdownType, "*"+helper.Title(property)+"*\n"+value, false, false)) + } + + resultAttachment.Blocks.BlockSet = append(resultAttachment.Blocks.BlockSet, propBlock) + } + + p.Attachments = append(p.Attachments, resultAttachment) + } + + return p +} + func (s *client) Send(result v1alpha2.PolicyReportResult) { - req, err := http.CreateJSONRequest(s.Name(), "POST", s.webhook, s.newPayload(result)) - if err != nil { + s.PostMessage(s.message(result)) +} + +func (s *client) BatchSend(report v1alpha2.ReportInterface, results []v1alpha2.PolicyReportResult) { + if report.GetScope() == nil { + for _, result := range results { + s.Send(result) + } + return } + s.PostMessage(s.batchMessage(report, results)) +} + +func (s *client) PostMessage(message *slack.WebhookMessage) { + req, err := http.CreateJSONRequest("POST", s.webhook, message) + if err != nil { + zap.L().Error(s.Name()+": PUSH FAILED", zap.Error(err)) + return + } + + for k, v := range s.headers { + req.Header.Set(k, v) + } + resp, err := s.client.Do(req) + http.ProcessHTTPResponse(s.Name(), resp, err) } -func (s *client) CleanUp(_ context.Context, _ v1alpha2.ReportInterface) {} +func (s *client) Type() target.ClientType { + return target.BatchSend +} // NewClient creates a new slack.client to send Results to Slack func NewClient(options Options) target.Client { return &client{ target.NewBaseClient(options.ClientOptions), - options.Webhook, options.Channel, + options.Webhook, options.HTTPClient, options.CustomFields, + options.Headers, } } diff --git a/pkg/target/teams/card.go b/pkg/target/teams/card.go new file mode 100644 index 00000000..d4154ab8 --- /dev/null +++ b/pkg/target/teams/card.go @@ -0,0 +1,55 @@ +package teams + +import ( + "github.com/atc0005/go-teams-notify/v2/adaptivecard" + + "github.com/kyverno/policy-reporter/pkg/helper" +) + +func newFactSet() adaptivecard.Element { + factSet := adaptivecard.Element{ + Type: adaptivecard.TypeElementFactSet, + } + + return factSet +} + +func newFactSetPointer() *adaptivecard.Element { + factSet := newFactSet() + + return &factSet +} + +func newSubTitle(title string) adaptivecard.Element { + text := adaptivecard.NewTextBlock(title, true) + text.Weight = adaptivecard.WeightBolder + text.IsSubtle = true + + return text +} + +func MapToColumnSet(list map[string]string) adaptivecard.Element { + i := 0 + + first := adaptivecard.NewColumn() + first.Items = append(first.Items, newFactSetPointer()) + + second := adaptivecard.NewColumn() + second.Items = append(second.Items, newFactSetPointer()) + + propBlock := adaptivecard.NewColumnSet() + propBlock.Columns = []adaptivecard.Column{first, second} + + for property, value := range list { + index := i % 2 + + propBlock.Columns[index].Items[0].Facts = append(propBlock.Columns[index].Items[0].Facts, adaptivecard.Fact{ + Title: helper.Title(property), + Value: value, + }) + + i++ + } + + return propBlock +} diff --git a/pkg/target/teams/teams.go b/pkg/target/teams/teams.go index cf84d7b5..ede446fb 100644 --- a/pkg/target/teams/teams.go +++ b/pkg/target/teams/teams.go @@ -2,11 +2,15 @@ package teams import ( "context" - "strings" - "time" + "fmt" + + "github.com/atc0005/go-teams-notify/v2/adaptivecard" + "go.uber.org/zap" + corev1 "k8s.io/api/core/v1" "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" "github.com/kyverno/policy-reporter/pkg/target" + "github.com/kyverno/policy-reporter/pkg/target/formatting" "github.com/kyverno/policy-reporter/pkg/target/http" ) @@ -15,125 +19,120 @@ type Options struct { target.ClientOptions Webhook string CustomFields map[string]string + Headers map[string]string HTTPClient http.Client } -type fact struct { - Name string `json:"name"` - Value string `json:"value"` -} - -type section struct { - Title string `json:"activityTitle"` - SubTitle string `json:"activitySubtitle"` - Text string `json:"text"` - Facts []fact `json:"facts,omitempty"` -} - -type payload struct { - Type string `json:"@type"` - Context string `json:"@context"` - Summary string `json:"summary,omitempty"` - ThemeColor string `json:"themeColor,omitempty"` - Sections []section `json:"sections"` -} - -var colors = map[v1alpha2.Priority]string{ - v1alpha2.DebugPriority: "68c2ff", - v1alpha2.InfoPriority: "36a64f", - v1alpha2.WarningPriority: "f2c744", - v1alpha2.CriticalPriority: "b80707", - v1alpha2.ErrorPriority: "e20b0b", -} - -func newPayload(result v1alpha2.PolicyReportResult, customFields map[string]string) payload { - facts := make([]fact, 0) - - facts = append(facts, fact{"Policy", result.Policy}) - - if result.Rule != "" { - facts = append(facts, fact{"Rule", result.Rule}) - } - - facts = append(facts, fact{"Priority", result.Priority.String()}) - - if result.Category != "" { - facts = append(facts, fact{"Category", result.Category}) - } - if result.Severity != "" { - facts = append(facts, fact{"Severity", string(result.Severity)}) - } - - if result.HasResource() { - res := result.GetResource() - - facts = append(facts, fact{"Kind", res.Kind}) - facts = append(facts, fact{"Name", res.Name}) - if res.UID != "" { - facts = append(facts, fact{"UID", string(res.UID)}) - } - if res.Namespace != "" { - facts = append(facts, fact{"Namespace", res.Namespace}) - } - if res.APIVersion != "" { - facts = append(facts, fact{"API Version", res.APIVersion}) - } - } - - for property, value := range result.Properties { - facts = append(facts, fact{strings.Title(property), value}) - } - for property, value := range customFields { - facts = append(facts, fact{strings.Title(property), value}) - } - - timestamp := time.Now() - if result.Timestamp.Seconds == 0 { - timestamp = time.Unix(result.Timestamp.Seconds, int64(result.Timestamp.Nanos)) - } - - sections := make([]section, 0, 1) - sections = append(sections, section{ - Title: "New Policy Report Result", - SubTitle: timestamp.Format(time.RFC3339), - Text: result.Message, - Facts: facts, - }) - - return payload{ - Type: "MessageCard", - Context: "http://schema.org/extensions", - Summary: result.Message, - ThemeColor: colors[result.Priority], - Sections: sections, - } -} - type client struct { target.BaseClient webhook string customFields map[string]string + headers map[string]string client http.Client } func (s *client) Send(result v1alpha2.PolicyReportResult) { - req, err := http.CreateJSONRequest(s.Name(), "POST", s.webhook, newPayload(result, s.customFields)) - if err != nil { - return - } - - resp, err := s.client.Do(req) - http.ProcessHTTPResponse(s.Name(), resp, err) + s.PostMessage(s.newMessage(result.GetResource(), []v1alpha2.PolicyReportResult{result})) } func (s *client) CleanUp(_ context.Context, _ v1alpha2.ReportInterface) {} +func (s *client) BatchSend(report v1alpha2.ReportInterface, results []v1alpha2.PolicyReportResult) { + if report.GetScope() == nil { + for _, r := range results { + s.Send(r) + } + } + + s.PostMessage(s.newMessage(report.GetScope(), results)) +} + +func (s *client) PostMessage(message *adaptivecard.Message) { + if err := message.Validate(); err != nil { + zap.L().Error(s.Name()+": PUSH FAILED", zap.Error(err)) + return + } + + req, err := http.CreateJSONRequest("POST", s.webhook, message) + if err != nil { + zap.L().Error(s.Name()+": PUSH FAILED", zap.Error(err)) + return + } + + for k, v := range s.headers { + req.Header.Set(k, v) + } + + resp, err := s.client.Do(req) + + http.ProcessHTTPResponse(s.Name(), resp, err) +} + +func (s *client) Type() target.ClientType { + return target.BatchSend +} + +func (s *client) newMessage(resource *corev1.ObjectReference, results []v1alpha2.PolicyReportResult) *adaptivecard.Message { + header := adaptivecard.NewContainer() + + if resource != nil { + header.AddElement(false, adaptivecard.NewTitleTextBlock(formatting.ResourceString(resource), true)) + } else { + header.AddElement(false, adaptivecard.NewTitleTextBlock("New PolicyReport Results", true)) + } + + header.AddElement(false, adaptivecard.NewTextBlock(fmt.Sprintf("Received %d new Policy Report Results", len(results)), true)) + + if len(s.customFields) > 0 { + header.AddElement(false, MapToColumnSet(s.customFields)) + } + + card := adaptivecard.NewCard() + card.SetFullWidth() + card.AddContainer(true, header) + + for _, result := range results { + stats := newFactSet() + stats.Facts = append(stats.Facts, adaptivecard.Fact{Title: "Status", Value: string(result.Result)}) + + if result.Severity != "" { + stats.Facts = append(stats.Facts, adaptivecard.Fact{Title: "Severity", Value: string(result.Severity)}) + } + + policy := fmt.Sprintf("Policy: %s", result.Policy) + + if result.Rule != "" { + policy = fmt.Sprintf("%s/%s", policy, result.Rule) + } + + r := adaptivecard.NewContainer() + r.Separator = true + r.Spacing = adaptivecard.SpacingLarge + r.AddElement(false, newSubTitle(policy)) + r.AddElement(false, adaptivecard.NewTextBlock(result.Category, true)) + r.AddElement(false, stats) + r.AddElement(false, adaptivecard.NewTextBlock(result.Message, true)) + + if len(result.Properties) > 0 { + r.AddElement(false, MapToColumnSet(result.Properties)) + } + + card.AddContainer(false, r) + } + + msg := adaptivecard.NewMessage() + msg.Attach(card) + + return msg +} + // NewClient creates a new teams.client to send Results to MS Teams func NewClient(options Options) target.Client { return &client{ target.NewBaseClient(options.ClientOptions), options.Webhook, options.CustomFields, + options.Headers, options.HTTPClient, } } diff --git a/pkg/target/teams/teams_test.go b/pkg/target/teams/teams_test.go index decc9473..0ec1f637 100644 --- a/pkg/target/teams/teams_test.go +++ b/pkg/target/teams/teams_test.go @@ -5,6 +5,8 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/assert" + "github.com/kyverno/policy-reporter/pkg/fixtures" "github.com/kyverno/policy-reporter/pkg/target" "github.com/kyverno/policy-reporter/pkg/target/teams" @@ -44,10 +46,6 @@ func Test_TeamsTarget(t *testing.T) { if err != nil { t.Fatal(err) } - - if payload["themeColor"] != "f2c744" { - t.Errorf("Unexpected ThemeColor %s", payload["themeColor"]) - } } client := teams.NewClient(teams.Options{ @@ -81,10 +79,6 @@ func Test_TeamsTarget(t *testing.T) { if err != nil { t.Fatal(err) } - - if payload["themeColor"] != "b80707" { - t.Errorf("Unexpected ThemeColor %s", payload["themeColor"]) - } } client := teams.NewClient(teams.Options{ @@ -105,10 +99,6 @@ func Test_TeamsTarget(t *testing.T) { if err != nil { t.Fatal(err) } - - if payload["themeColor"] != "36a64f" { - t.Errorf("Unexpected ThemeColor %s", payload["themeColor"]) - } } client := teams.NewClient(teams.Options{ @@ -129,10 +119,6 @@ func Test_TeamsTarget(t *testing.T) { if err != nil { t.Fatal(err) } - - if payload["themeColor"] != "e20b0b" { - t.Errorf("Unexpected ThemeColor %s", payload["themeColor"]) - } } client := teams.NewClient(teams.Options{ @@ -165,10 +151,6 @@ func Test_TeamsTarget(t *testing.T) { if err != nil { t.Fatal(err) } - - if payload["themeColor"] != "68c2ff" { - t.Errorf("Unexpected ThemeColor %s", payload["themeColor"]) - } } client := teams.NewClient(teams.Options{ @@ -191,8 +173,18 @@ func Test_TeamsTarget(t *testing.T) { HTTPClient: testClient{}, }) - if client.Name() != "Teams" { - t.Errorf("Unexpected Name %s", client.Name()) - } + assert.Equal(t, "Teams", client.Name()) + }) + t.Run("SupportBatchSend", func(t *testing.T) { + client := teams.NewClient(teams.Options{ + ClientOptions: target.ClientOptions{ + Name: "Teams", + }, + Webhook: "http://hook.teams:80", + CustomFields: map[string]string{"Cluster": "Name"}, + HTTPClient: testClient{}, + }) + + assert.Equal(t, target.BatchSend, client.Type()) }) } diff --git a/pkg/target/telegram/telegram.go b/pkg/target/telegram/telegram.go index ca67fd12..dbbf075b 100644 --- a/pkg/target/telegram/telegram.go +++ b/pkg/target/telegram/telegram.go @@ -27,7 +27,7 @@ func escape(text interface{}) string { return replacer.Replace(fmt.Sprintf("%v", text)) } -var notificationTempl = `*\[Policy Reporter\] \[{{ .Priority }}\] {{ escape (or .Result.Policy .Result.Rule) }}* +var notificationTempl = `*\[Policy Reporter\] \[{{ .Result.Severity }}\] {{ escape (or .Result.Policy .Result.Rule) }}* {{- if .Resource }} *Resource*: {{ .Resource.Kind }} {{ if .Resource.Namespace }}{{ escape .Resource.Namespace }}/{{ end }}{{ escape .Resource.Name }} @@ -118,16 +118,10 @@ func (e *client) Send(result v1alpha2.PolicyReportResult) { res = result.GetResource() } - prio := result.Priority.String() - if prio == "" { - prio = v1alpha2.DebugPriority.String() - } - err = ttmpl.Execute(&textBuffer, values{ Result: result, Time: time.Now(), Resource: res, - Priority: prio, }) if err != nil { zap.L().Error(e.Name()+": PUSH FAILED", zap.Error(err)) @@ -136,7 +130,7 @@ func (e *client) Send(result v1alpha2.PolicyReportResult) { payload.Text = textBuffer.String() - req, err := http.CreateJSONRequest(e.Name(), "POST", e.host, payload) + req, err := http.CreateJSONRequest("POST", e.host, payload) if err != nil { zap.L().Error(e.Name()+": PUSH FAILED", zap.Error(err)) fmt.Println(err) @@ -153,6 +147,16 @@ func (e *client) Send(result v1alpha2.PolicyReportResult) { func (e *client) CleanUp(_ context.Context, _ v1alpha2.ReportInterface) {} +func (e *client) Reset(_ context.Context) error { + return nil +} + +func (e *client) BatchSend(_ v1alpha2.ReportInterface, _ []v1alpha2.PolicyReportResult) {} + +func (e *client) Type() target.ClientType { + return target.SingleSend +} + // NewClient creates a new loki.client to send Results to Elasticsearch func NewClient(options Options) target.Client { return &client{ diff --git a/pkg/target/ui/ui.go b/pkg/target/ui/ui.go deleted file mode 100644 index c6437708..00000000 --- a/pkg/target/ui/ui.go +++ /dev/null @@ -1,43 +0,0 @@ -package ui - -import ( - "context" - - "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" - "github.com/kyverno/policy-reporter/pkg/target" - "github.com/kyverno/policy-reporter/pkg/target/http" -) - -// Options to configure the Discord target -type Options struct { - target.ClientOptions - Host string - HTTPClient http.Client -} - -type client struct { - target.BaseClient - host string - client http.Client -} - -func (e *client) Send(result v1alpha2.PolicyReportResult) { - req, err := http.CreateJSONRequest(e.Name(), "POST", e.host, http.NewJSONResult(result)) - if err != nil { - return - } - - resp, err := e.client.Do(req) - http.ProcessHTTPResponse(e.Name(), resp, err) -} - -func (e *client) CleanUp(_ context.Context, _ v1alpha2.ReportInterface) {} - -// NewClient creates a new loki.client to send Results to Elasticsearch -func NewClient(options Options) target.Client { - return &client{ - target.NewBaseClient(options.ClientOptions), - options.Host + "/api/push", - options.HTTPClient, - } -} diff --git a/pkg/target/ui/ui_test.go b/pkg/target/ui/ui_test.go deleted file mode 100644 index 9da0c6d5..00000000 --- a/pkg/target/ui/ui_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package ui_test - -import ( - "net/http" - "testing" - - "github.com/kyverno/policy-reporter/pkg/fixtures" - "github.com/kyverno/policy-reporter/pkg/target" - "github.com/kyverno/policy-reporter/pkg/target/ui" -) - -type testClient struct { - callback func(req *http.Request) - statusCode int -} - -func (c testClient) Do(req *http.Request) (*http.Response, error) { - c.callback(req) - - return &http.Response{ - StatusCode: c.statusCode, - }, nil -} - -func Test_UITarget(t *testing.T) { - t.Run("Send", func(t *testing.T) { - callback := func(req *http.Request) { - if contentType := req.Header.Get("Content-Type"); contentType != "application/json; charset=utf-8" { - t.Errorf("Unexpected Content-Type: %s", contentType) - } - - if agend := req.Header.Get("User-Agent"); agend != "Policy-Reporter" { - t.Errorf("Unexpected Host: %s", agend) - } - - if url := req.URL.String(); url != "http://localhost:8080/api/push" { - t.Errorf("Unexpected Host: %s", url) - } - } - - client := ui.NewClient(ui.Options{ - ClientOptions: target.ClientOptions{ - Name: "UI", - }, - Host: "http://localhost:8080", - HTTPClient: testClient{callback, 200}, - }) - client.Send(fixtures.CompleteTargetSendResult) - }) - t.Run("Name", func(t *testing.T) { - client := ui.NewClient(ui.Options{ - ClientOptions: target.ClientOptions{ - Name: "UI", - }, - Host: "http://localhost:8080", - HTTPClient: testClient{}, - }) - - if client.Name() != "UI" { - t.Errorf("Unexpected Name %s", client.Name()) - } - }) -} diff --git a/pkg/target/webhook/webhook.go b/pkg/target/webhook/webhook.go index cedc6e93..032b7378 100644 --- a/pkg/target/webhook/webhook.go +++ b/pkg/target/webhook/webhook.go @@ -1,8 +1,6 @@ package webhook import ( - "context" - "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" "github.com/kyverno/policy-reporter/pkg/target" "github.com/kyverno/policy-reporter/pkg/target/http" @@ -40,7 +38,7 @@ func (e *client) Send(result v1alpha2.PolicyReportResult) { result.Properties = props } - req, err := http.CreateJSONRequest(e.Name(), "POST", e.host, http.NewJSONResult(result)) + req, err := http.CreateJSONRequest("POST", e.host, http.NewJSONResult(result)) if err != nil { return } @@ -53,7 +51,9 @@ func (e *client) Send(result v1alpha2.PolicyReportResult) { http.ProcessHTTPResponse(e.Name(), resp, err) } -func (e *client) CleanUp(_ context.Context, _ v1alpha2.ReportInterface) {} +func (e *client) Type() target.ClientType { + return target.SingleSend +} // NewClient creates a new loki.client to send Results to Elasticsearch func NewClient(options Options) target.Client { diff --git a/pkg/target/webhook/webhook_test.go b/pkg/target/webhook/webhook_test.go index e429d010..d7290cd3 100644 --- a/pkg/target/webhook/webhook_test.go +++ b/pkg/target/webhook/webhook_test.go @@ -76,4 +76,20 @@ func Test_UITarget(t *testing.T) { t.Errorf("Unexpected Name %s", client.Name()) } }) + t.Run("Request Error", func(t *testing.T) { + callback := func(req *http.Request) error { + t.Fail() + + return nil + } + + client := webhook.NewClient(webhook.Options{ + ClientOptions: target.ClientOptions{ + Name: "UI", + }, + Host: "\\localhost:8080", + HTTPClient: testClient{callback, 200}, + }) + client.Send(fixtures.CompleteTargetSendResult) + }) } diff --git a/pkg/validate/model.go b/pkg/validate/model.go index 22468fea..54ffc0a5 100644 --- a/pkg/validate/model.go +++ b/pkg/validate/model.go @@ -1,10 +1,15 @@ package validate type RuleSets struct { - Exclude []string - Include []string + Exclude []string + Include []string + Selector map[string]string } func (r RuleSets) Count() int { return len(r.Exclude) + len(r.Include) } + +func (r RuleSets) Enabled() bool { + return r.Count() > 0 +} diff --git a/policy.yaml b/policy.yaml new file mode 100644 index 00000000..ed32e280 --- /dev/null +++ b/policy.yaml @@ -0,0 +1,22 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: require-labels +spec: + validationFailureAction: Audit + rules: + - name: check-for-labels + match: + any: + - resources: + kinds: + - Pod + namespaces: + - policy-reporter + + validate: + message: "label 'app.kubernetes.io/test' is required" + pattern: + metadata: + labels: + app.kubernetes.io/test: "?*" \ No newline at end of file diff --git a/scripts/kind.yaml b/scripts/kind.yaml new file mode 100644 index 00000000..c3f5f173 --- /dev/null +++ b/scripts/kind.yaml @@ -0,0 +1,34 @@ +kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +kubeadmConfigPatches: + - |- + kind: ClusterConfiguration + controllerManager: + extraArgs: + bind-address: 0.0.0.0 + etcd: + local: + extraArgs: + listen-metrics-urls: http://0.0.0.0:2382 + scheduler: + extraArgs: + bind-address: 0.0.0.0 + - |- + kind: KubeProxyConfiguration + metricsBindAddress: 0.0.0.0 +nodes: + - role: control-plane + kubeadmConfigPatches: + - |- + kind: InitConfiguration + nodeRegistration: + kubeletExtraArgs: + node-labels: "ingress-ready=true" + extraPortMappings: + - containerPort: 80 + hostPort: 80 + protocol: TCP + - containerPort: 443 + hostPort: 443 + protocol: TCP + - role: worker diff --git a/test b/test new file mode 100644 index 00000000..b586b48c --- /dev/null +++ b/test @@ -0,0 +1,2 @@ +Pushed: ghcr.io/fjogeleit/charts/policy-reporter:3.0.0-beta.18 +Digest: sha256:ebc720b178dbbdc5def056e0d8c57a9475180d087c68c7afdf64ea5bbaff6eab