├── .github ├── actions │ ├── build │ │ └── action.yml │ ├── context │ │ └── action.yaml │ ├── grype │ │ └── action.yaml │ ├── k3s-cluster │ │ └── action.yaml │ ├── k8s-version-config │ │ └── action.yaml │ ├── trivy-config │ │ └── action.yaml │ └── trivy-image │ │ └── action.yaml ├── dependabot.yml └── workflows │ ├── .reusable-build.yml │ ├── .reusable-ci.yml │ ├── .reusable-cleanup-registry.yml │ ├── .reusable-compliance.yml │ ├── .reusable-docs.yml │ ├── .reusable-integration-test.yml │ ├── .reusable-sast.yml │ ├── .reusable-sca.yml │ ├── .reusable-unit-test.yml │ ├── nightly-build.yml │ ├── nightly.yaml │ ├── pr.yml │ ├── pr2main.yml │ ├── push.yml │ ├── semgrep.yml │ └── tag.yml ├── .gitignore ├── .kube-linter └── config.yaml ├── .python-version ├── LICENSE.md ├── Makefile ├── README.md ├── build ├── Dockerfile └── harden.sh ├── charts └── semgr8s │ ├── Chart.yaml │ ├── rules │ └── test-semgr8s-forbidden-label.yaml │ ├── templates │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── env.yaml │ ├── role.yaml │ ├── rolebinding.yaml │ ├── rules.yaml │ ├── service.yaml │ ├── serviceaccount.yaml │ └── webhook.yaml │ └── values.yaml ├── docs ├── README.md ├── SECURITY.md ├── assets │ ├── semgr8s-architecture-dark.png │ ├── semgr8s-architecture.excalidraw │ ├── semgr8s-architecture.png │ ├── semgr8s-demo.gif │ ├── semgr8s-design-dark.png │ ├── semgr8s-design.excalidraw │ ├── semgr8s-design.png │ ├── semgr8s-logo-full-dark.png │ ├── semgr8s-logo-full-dark.svg │ ├── semgr8s-logo-full-light.png │ ├── semgr8s-logo-full-light.svg │ ├── semgr8s-logo-single.png │ ├── semgr8s-logo-single.svg │ ├── semgr8s-logo-vertical-light.png │ ├── semgr8s-logo-vertical-light.svg │ ├── semgr8s-logo.png │ ├── semgr8s-logo.svg │ ├── semgrep-logo-dark.svg │ ├── semgrep-logo-light.svg │ ├── sse-logo-dark.svg │ └── sse-logo-light.svg ├── concept.md ├── examples │ ├── deny-default-namespace.md │ ├── forbidden-namespaced-label.md │ ├── forbidden-pod-label.md │ ├── forbidden-workload-label.md │ ├── restrict-image-registry.md │ ├── template-autofix-rule.md │ └── template-rule.md ├── javascripts │ └── tablesort.js ├── overrides │ └── main.html └── usage.md ├── mkdocs.yml ├── poetry.lock ├── pyproject.toml ├── rules ├── deny-default-namespace.yaml ├── forbidden-namespaced-label.yaml ├── forbidden-pod-label.yaml ├── forbidden-workload-label.yaml ├── restrict-image-registry.yaml ├── template-autofix-rule.yaml ├── template-rule.yaml └── tests │ ├── deny-default-namespace.test.yaml │ ├── forbidden-namespaced-label.test.fixed.yaml │ ├── forbidden-namespaced-label.test.yaml │ ├── forbidden-pod-label.test.fixed.yaml │ ├── forbidden-pod-label.test.yaml │ ├── forbidden-workload-label.test.fixed.yaml │ ├── forbidden-workload-label.test.yaml │ ├── restrict-image-registry.test.yaml │ ├── template-autofix-rule.test.fixed.yaml │ ├── template-autofix-rule.test.yaml │ └── template-rule.test.yaml ├── semgr8s ├── __init__.py ├── __main__.py ├── app.py ├── files.py ├── k8s_api.py └── updater.py └── tests ├── README.md ├── __init__.py ├── conftest.py ├── data ├── sample_admission_requests │ ├── admission_request_deployments.json │ ├── admission_request_deployments_forbiddenlabel.json │ ├── admission_request_empty.json │ ├── admission_request_no_request.json │ ├── admission_request_no_request_uid.json │ ├── admission_request_pods.json │ └── admission_request_pods_forbiddenlabel.json ├── sample_k8s_resources │ ├── configmaps.json │ ├── configmaps_broken_nodata.json │ ├── configmaps_broken_nojson.json │ ├── configmaps_multiplerulesinmap.json │ ├── configmaps_semgr8ns.json │ ├── deployments.json │ ├── pods.json │ └── replicasets.json └── scanfile_nosc_pod.yaml ├── demo ├── 00_test-namespace.yaml ├── 20_passing-deployment.yaml └── 40_failing-deployment.yaml ├── integration ├── README.md ├── data │ ├── 00_test_namespaces.yaml │ ├── 01_semgr8ns_namespace.yaml │ ├── 20_compliant_pod.yaml │ ├── 30_testlabel_pod.yaml │ ├── 40_testlabel_pod.yaml │ ├── 41_nosc_pod.yaml │ ├── 42_privileged_pod.yaml │ ├── 43_hostnetwork_pod.yaml │ ├── 44_multifail_pod.yaml │ └── 45_other_testlabel_pod.yaml ├── main.sh ├── rules │ └── test-semgr8s-no-foobar-label.yaml ├── scripts │ ├── audit.sh │ ├── autofix.sh │ ├── basic.sh │ ├── common.sh │ ├── remote_rules.sh │ └── semgrep_login.sh └── test_cases │ ├── audit.yaml │ ├── autofix.yaml │ ├── basic.yaml │ ├── remote_rules.yaml │ └── semgrep_login.yaml ├── test_app.py ├── test_k8s_api.py └── test_updater.py /.github/actions/grype/action.yaml: -------------------------------------------------------------------------------- 1 | name: grype 2 | description: 'Run Grype on image' 3 | inputs: 4 | image: 5 | description: 'Image name' 6 | required: true 7 | registry: 8 | description: 'Registry to login to pull image, e.g. "ghcr.io" for GHCR, leave empty if image is public' 9 | required: false 10 | default: '' 11 | repo_owner: 12 | description: 'Name of repository owner, e.g. "github.repository_owner" for ghcr.io' 13 | required: false 14 | repo_token: 15 | description: 'Access token for repository owner, e.g. "secrets.GITHUB_TOKEN" for ghcr.io' 16 | required: false 17 | output: 18 | description: 'Grype output either "sarif" (GITHUB_TOKEN with security-events:write) or print results as "table" and fail on error' 19 | required: false 20 | runs: 21 | using: "composite" 22 | steps: 23 | - name: Login with registry 24 | if: inputs.registry != '' 25 | uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a # v2.1.0 26 | with: 27 | registry: ${{ inputs.registry }} 28 | username: ${{ inputs.repo_owner }} 29 | password: ${{ inputs.repo_token }} 30 | - name: Scan 31 | if: inputs.output == 'table' 32 | uses: anchore/scan-action@dafbc97d7259af88b61bd260f2fde565d0668a72 # v3.3.4 33 | with: 34 | image: ${{ inputs.image }} 35 | fail-build: true 36 | severity-cutoff: high 37 | output-format: table 38 | - name: Scan 39 | id: scan 40 | if: inputs.output == 'sarif' 41 | uses: anchore/scan-action@dafbc97d7259af88b61bd260f2fde565d0668a72 # v3.3.4 42 | with: 43 | image: ${{ inputs.image }} 44 | fail-build: false 45 | output-format: sarif 46 | - name: Upload 47 | if: inputs.output == 'sarif' 48 | uses: github/codeql-action/upload-sarif@32dc499307d133bb5085bae78498c0ac2cf762d5 # v2.2.5 49 | with: 50 | sarif_file: ${{ steps.scan.outputs.sarif }} 51 | -------------------------------------------------------------------------------- /.github/actions/k3s-cluster/action.yaml: -------------------------------------------------------------------------------- 1 | # Adjusted from https://github.com/jupyterhub/action-k3s-helm 2 | --- 3 | name: K3S with Helm 4 | description: | 5 | Install Kubernetes (K3S) and Helm. 6 | 7 | inputs: 8 | k3s-channel: 9 | description: K3S channel (https://update.k3s.io/v1-release/channels) 10 | required: false 11 | default: "" 12 | 13 | outputs: 14 | kubeconfig: 15 | description: Path to kubeconfig file 16 | value: ${{ steps.set-versions.outputs.kubeconfig }} 17 | k3s-version: 18 | description: "Installed k3s version, such as v1.20.0+k3s2" 19 | value: "${{ steps.set-versions.outputs.k3s-version }}" 20 | k8s-version: 21 | description: "Installed k8s version, such as v1.20.0" 22 | value: "${{ steps.set-versions.outputs.k8s-version }}" 23 | helm-version: 24 | description: "Installed helm version, such as v3.4.2" 25 | value: "${{ steps.set-versions.outputs.helm-version }}" 26 | 27 | runs: 28 | using: "composite" 29 | steps: 30 | - name: Setup k3s ${{ inputs.k3s-channel }} 31 | run: | 32 | curl -sfL https://get.k3s.io | INSTALL_K3S_CHANNEL="${{ inputs.k3s-channel }}" sh -s - 33 | shell: bash 34 | 35 | # By providing a kubeconfig owned by the current user with 600 permissions, 36 | # kubectl becomes usable without sudo, and helm won't emit warnings about 37 | # bloated access to group/world. 38 | - name: Prepare a kubeconfig in ~/.kube/config 39 | run: | 40 | mkdir -p ~/.kube 41 | sudo cat /etc/rancher/k3s/k3s.yaml > "$HOME/.kube/config" 42 | chmod 600 "$HOME/.kube/config" 43 | echo "KUBECONFIG=$HOME/.kube/config" >> $GITHUB_ENV 44 | shell: bash 45 | 46 | - name: Setup Helm 47 | run: | 48 | curl -sf https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash 49 | shell: bash 50 | 51 | - name: Set version output 52 | id: set-versions 53 | run: | 54 | echo "::group::Set version output" 55 | echo "kubeconfig=$HOME/.kube/config" >> $GITHUB_OUTPUT 56 | echo "k3s-version=$(k3s --version | grep 'k3s' | sed 's/.*\(v[0-9][^ ]*\).*/\1/')" >> $GITHUB_OUTPUT 57 | echo "k8s-version=$(k3s --version | grep 'k3s' | sed 's/.*\(v[0-9][^+]*\).*/\1/')" >> $GITHUB_OUTPUT 58 | echo "::endgroup::" 59 | shell: bash 60 | 61 | - name: Wait for coredns, metrics server, traefik 62 | run: | 63 | # Wait for a few seconds to allow deployments spin up 64 | sleep 10 65 | 66 | kubectl rollout status --watch --timeout 300s deployment/coredns -n kube-system 67 | 68 | kubectl rollout status --watch --timeout 300s deployment/metrics-server -n kube-system 69 | 70 | kubectl wait --for=condition=complete --timeout=300s job/helm-install-traefik-crd -n kube-system || true 71 | kubectl wait --for=condition=complete --timeout=300s job/helm-install-traefik -n kube-system || true 72 | kubectl rollout status --watch --timeout 300s deployment/traefik -n kube-system 73 | shell: bash 74 | 75 | -------------------------------------------------------------------------------- /.github/actions/k8s-version-config/action.yaml: -------------------------------------------------------------------------------- 1 | name: k8s-version-config 2 | description: 'action to prepare testing different k8s versions' 3 | inputs: 4 | k8s-version: 5 | description: 'k8s version to be tested' 6 | required: true 7 | runs: 8 | using: "composite" 9 | steps: 10 | - name: Install yq and bash 11 | run: | 12 | sudo snap install yq 13 | sudo apt update 14 | sudo apt install bash -y 15 | shell: bash 16 | - uses: ./.github/actions/k3s-cluster 17 | with: 18 | k3s-channel: ${{ inputs.k8s-version }} 19 | - name: Adjust Configuration 20 | run: | 21 | if [[ $(echo "${{ inputs.k8s-version }}" | tail -c 3) -lt "19" ]]; then 22 | yq e 'del(.kubernetes.deployment.securityContext.seccompProfile)' -i charts/semgr8s/values.yaml 23 | yq e '.kubernetes.deployment.annotations."seccomp.security.alpha.kubernetes.io/pod" = "runtime/default"' -i charts/semgr8s/values.yaml 24 | fi 25 | shell: bash 26 | -------------------------------------------------------------------------------- /.github/actions/trivy-config/action.yaml: -------------------------------------------------------------------------------- 1 | name: trivy-config 2 | description: 'Run Trivy on config' 3 | inputs: 4 | output: 5 | description: 'Trivy output either "sarif" (GITHUB_TOKEN with security-events:write) or print results as "table" and fail on error' 6 | required: false 7 | runs: 8 | using: "composite" 9 | steps: 10 | - name: Create reports folder 11 | run: | 12 | mkdir reports 13 | shell: bash 14 | - name: Render Helm charts 15 | run: | 16 | mkdir deployment 17 | helm template charts/semgr8s > deployment/deployment.yaml 18 | shell: bash 19 | - name: Scan deployment.yaml 20 | uses: aquasecurity/trivy-action@fbd16365eb88e12433951383f5e99bd901fc618f # v0.12.0 21 | if: inputs.output == 'table' 22 | with: 23 | scan-type: "config" 24 | scan-ref: "deployment" 25 | format: 'table' 26 | - name: Scan Dockerfiles 27 | uses: aquasecurity/trivy-action@fbd16365eb88e12433951383f5e99bd901fc618f # v0.12.0 28 | if: inputs.output == 'table' 29 | with: 30 | scan-type: "config" 31 | scan-ref: "build" 32 | format: 'table' 33 | - name: Scan deployment.yaml 34 | uses: aquasecurity/trivy-action@fbd16365eb88e12433951383f5e99bd901fc618f # v0.12.0 35 | if: inputs.output == 'sarif' 36 | with: 37 | scan-type: "config" 38 | scan-ref: "deployment" 39 | format: 'sarif' 40 | output: 'reports/trivy-k8s-results.sarif' 41 | - name: Scan Dockerfiles 42 | uses: aquasecurity/trivy-action@fbd16365eb88e12433951383f5e99bd901fc618f # v0.12.0 43 | if: inputs.output == 'sarif' 44 | with: 45 | scan-type: "config" 46 | scan-ref: "build" 47 | format: 'sarif' 48 | output: 'reports/trivy-docker-results.sarif' 49 | - name: Upload 50 | uses: github/codeql-action/upload-sarif@32dc499307d133bb5085bae78498c0ac2cf762d5 # v2.2.5 51 | if: inputs.output == 'sarif' 52 | with: 53 | sarif_file: 'reports' 54 | -------------------------------------------------------------------------------- /.github/actions/trivy-image/action.yaml: -------------------------------------------------------------------------------- 1 | name: trivy-image 2 | description: 'Run Trivy on image' 3 | inputs: 4 | image: 5 | description: 'Image name' 6 | required: true 7 | registry: 8 | description: 'Registry to login to pull image, e.g. "ghcr.io" for GHCR, leave empty if image is public' 9 | required: false 10 | default: '' 11 | repo_owner: 12 | description: 'Name of repository owner, e.g. "github.repository_owner" for ghcr.io' 13 | required: false 14 | repo_token: 15 | description: 'Access token for repository owner, e.g. "secrets.GITHUB_TOKEN" for ghcr.io' 16 | required: false 17 | output: 18 | description: 'Trivy output either "sarif" (GITHUB_TOKEN with security-events:write) or print results as "table" and fail on error' 19 | required: false 20 | runs: 21 | using: "composite" 22 | steps: 23 | - name: Login with registry 24 | if: inputs.registry != '' 25 | uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a # v2.1.0 26 | with: 27 | registry: ${{ inputs.registry }} 28 | username: ${{ inputs.repo_owner }} 29 | password: ${{ inputs.repo_token }} 30 | - name: Create reports folder 31 | run: | 32 | mkdir reports 33 | shell: sh 34 | - name: Run Trivy on image 35 | if: inputs.output == 'sarif' 36 | uses: aquasecurity/trivy-action@fbd16365eb88e12433951383f5e99bd901fc618f # v0.12.0 37 | with: 38 | image-ref: ${{ inputs.image }} 39 | scan-type: "image" 40 | format: 'sarif' 41 | output: 'reports/trivy-vuln-results.sarif' 42 | - name: Run Trivy on image 43 | if: inputs.output == 'table' 44 | uses: aquasecurity/trivy-action@fbd16365eb88e12433951383f5e99bd901fc618f # v0.12.0 45 | with: 46 | image-ref: ${{ inputs.image }} 47 | scan-type: "image" 48 | exit-code: 1 49 | format: 'table' 50 | - name: Upload 51 | if: inputs.output == 'sarif' 52 | uses: github/codeql-action/upload-sarif@32dc499307d133bb5085bae78498c0ac2cf762d5 # v2.2.5 53 | with: 54 | sarif_file: 'reports' 55 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | commit-message: 8 | prefix: "update" 9 | insecure-external-code-execution: "deny" 10 | target-branch: "dev" 11 | groups: 12 | pip-packages: 13 | patterns: 14 | - "*" 15 | - package-ecosystem: "docker" 16 | directory: "/build" 17 | schedule: 18 | interval: "daily" 19 | commit-message: 20 | prefix: "update" 21 | target-branch: "dev" 22 | groups: 23 | docker-packages: 24 | patterns: 25 | - "*" 26 | - package-ecosystem: "github-actions" 27 | directory: "/" 28 | schedule: 29 | interval: "daily" 30 | commit-message: 31 | prefix: "update" 32 | target-branch: "dev" 33 | groups: 34 | gh-actions-packages: 35 | patterns: 36 | - "*" 37 | 38 | -------------------------------------------------------------------------------- /.github/workflows/.reusable-build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | permissions: {} 4 | 5 | on: 6 | workflow_call: 7 | inputs: 8 | skip: 9 | description: "Want to skip running certain jobs 'none', 'non-required', 'all'?" 10 | type: string 11 | default: "none" 12 | outputs: 13 | chart_version: 14 | description: "semgr8s Helm chart version" 15 | value: ${{ jobs.context.outputs.chart_version }} 16 | original_registry: 17 | description: "Public semgr8s registry" 18 | value: ${{ jobs.context.outputs.original_registry }} 19 | original_repo: 20 | description: "Public semgr8s repo" 21 | value: ${{ jobs.context.outputs.original_repo }} 22 | original_tag: 23 | description: "Current semgr8s tag, i.e. version" 24 | value: ${{ jobs.context.outputs.original_tag }} 25 | original_image: 26 | description: "Full semgr8s image reference, i.e. registry + repository + tag" 27 | value: ${{ jobs.context.outputs.original_image }} 28 | build_registry: 29 | description: "Workflow build registry used for testing" 30 | value: ${{ jobs.context.outputs.build_registry }} 31 | build_repo: 32 | description: "Workflow build repository used for testing" 33 | value: ${{ jobs.context.outputs.build_repo }} 34 | build_tag: 35 | description: "Workflow build tag used for testing (unique for each run)" 36 | value: ${{ jobs.context.outputs.build_tag }} 37 | branch_tag: 38 | description: "Branch tag used for all builds on branch" 39 | value: ${{ jobs.context.outputs.branch_tag }} 40 | build_image: 41 | description: "Workflow build image used for testing, i.e. registry + repository + tag" 42 | value: ${{ jobs.context.outputs.build_image }} 43 | build_labels: 44 | description: "Repository- and workflow-specific build labels" 45 | value: ${{ jobs.context.outputs.build_labels }} 46 | 47 | jobs: 48 | context: 49 | runs-on: ubuntu-latest 50 | if: inputs.skip != 'all' 51 | permissions: {} 52 | outputs: 53 | chart_version: ${{ steps.get_context.outputs.chart_version }} 54 | original_registry: ${{ steps.get_context.outputs.original_registry }} 55 | original_repo: ${{ steps.get_context.outputs.original_repo }} 56 | original_image: ${{ steps.get_context.outputs.original_image }} 57 | original_tag: ${{ steps.get_context.outputs.original_tag }} 58 | build_registry: ${{ steps.get_context.outputs.build_registry }} 59 | build_repo: ${{ steps.get_context.outputs.build_repo }} 60 | build_tag: ${{ steps.get_context.outputs.build_tag }} 61 | ref_tags: ${{ steps.get_context.outputs.ref_tags }} 62 | build_image: ${{ steps.get_context.outputs.build_image }} 63 | build_labels: ${{ steps.get_context.outputs.build_labels }} 64 | steps: 65 | - name: Checkout code 66 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 67 | - name: Get context 68 | id: get_context 69 | uses: ./.github/actions/context 70 | 71 | build: 72 | runs-on: ubuntu-latest 73 | if: | 74 | inputs.skip != 'non-required' && 75 | inputs.skip != 'all' 76 | needs: [context] 77 | permissions: 78 | id-token: write 79 | packages: write 80 | steps: 81 | - name: Checkout code 82 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 83 | - name: Build semgr8s 84 | id: build 85 | uses: ./.github/actions/build 86 | with: 87 | image_registry: ${{ needs.context.outputs.build_registry }} 88 | image_repo: ${{ needs.context.outputs.build_repo }} 89 | image_tag: ${{ needs.context.outputs.build_tag }} 90 | ref_tags: ${{ needs.context.outputs.ref_tags }} 91 | image_labels: ${{ needs.context.outputs.build_labels }} 92 | repo_owner: ${{ github.repository_owner }} 93 | repo_token: ${{ secrets.GITHUB_TOKEN }} 94 | -------------------------------------------------------------------------------- /.github/workflows/.reusable-cleanup-registry.yml: -------------------------------------------------------------------------------- 1 | name: cleanup registry 2 | 3 | on: 4 | workflow_call: 5 | 6 | permissions: {} 7 | 8 | jobs: 9 | cleanup-registry: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Cleanup test images 13 | uses: snok/container-retention-policy@4f22ef80902ad409ed55a99dc5133cc1250a0d03 # v3.0.0 14 | with: 15 | image-names: semgr8s-test 16 | cut-off: three weeks ago UTC+1 17 | timestamp-to-use: updated_at 18 | account-type: org 19 | org-name: semgr8ns 20 | token: ${{ secrets.GHCR_PAT }} 21 | - name: Cleanup dangling images without tag 22 | uses: snok/container-retention-policy@4f22ef80902ad409ed55a99dc5133cc1250a0d03 # v3.0.0 23 | with: 24 | image-names: semgr8s* 25 | untagged-only: true 26 | cut-off: four hours ago UTC+1 27 | timestamp-to-use: updated_at 28 | account-type: org 29 | org-name: semgr8ns 30 | token: ${{ secrets.GHCR_PAT }} 31 | # - name: Cleanup all images 32 | # uses: snok/container-retention-policy@4f22ef80902ad409ed55a99dc5133cc1250a0d03 # v3.0.0 33 | # with: 34 | # image-names: semgr8s 35 | # skip-tags: main, dev, "v*.*.*", "sha256-*" 36 | # cut-off: four days ago UTC+1 37 | # timestamp-to-use: updated_at 38 | # account-type: org 39 | # org-name: semgr8ns 40 | # token: ${{ secrets.GHCR_PAT }} 41 | -------------------------------------------------------------------------------- /.github/workflows/.reusable-compliance.yml: -------------------------------------------------------------------------------- 1 | name: compliance 2 | 3 | permissions: read-all 4 | 5 | on: 6 | workflow_call: 7 | inputs: 8 | skip: 9 | description: "Want to skip running certain jobs 'none', 'non-required', 'all'?" 10 | type: string 11 | default: "none" 12 | 13 | jobs: 14 | ossf-scorecard: 15 | runs-on: ubuntu-latest 16 | if: | 17 | (github.ref_name == 'main' || github.event_name == 'pull_request') && 18 | inputs.skip != 'non-required' && 19 | inputs.skip != 'all' 20 | permissions: 21 | pull-requests: write 22 | security-events: write 23 | steps: 24 | - name: Checkout code 25 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 26 | with: 27 | persist-credentials: false 28 | - name: Analyze 29 | uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 30 | with: 31 | results_file: results.sarif 32 | results_format: sarif 33 | repo_token: ${{ secrets.SCORECARD_TOKEN }} 34 | publish_results: false #TODO: reactivate when working again 35 | - name: Upload 36 | uses: github/codeql-action/upload-sarif@dd196fa9ce80b6bacc74ca1c32bd5b0ba22efca7 # v3.28.3 37 | with: 38 | sarif_file: results.sarif 39 | 40 | dependency-review: 41 | name: dependency review 42 | runs-on: ubuntu-latest 43 | if: | 44 | github.event_name == 'pull_request' && 45 | inputs.skip != 'non-required' && 46 | inputs.skip != 'all' 47 | permissions: 48 | contents: write 49 | pull-requests: write 50 | steps: 51 | - name: Checkout code 52 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 53 | - name: Review 54 | uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0 55 | with: 56 | comment-summary-in-pr: always 57 | 58 | check-commit-message: 59 | runs-on: ubuntu-latest 60 | if: | 61 | github.event_name == 'pull_request' && 62 | inputs.skip != 'all' 63 | permissions: {} 64 | steps: 65 | - name: Checkout code 66 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 67 | with: 68 | ref: ${{ github.event.pull_request.head.sha }} # Otherwise will checkout merge commit, which isn't conform 69 | fetch-depth: ${{ github.event.pull_request.commits }} # Fetch all commits of the MR, but only those 70 | - name: Check commit messages for conformity 71 | run: | 72 | echo "Commits between dev branch and current SHA:" 73 | COMMITS=$(git log --pretty=%H) 74 | echo "${COMMITS}" 75 | EXIT=0 76 | COMMIT_MSGS=$(git log --pretty=%s) # show subject only 77 | for commit in ${COMMITS}; do 78 | MSG=$(git log ${commit} -n1 --pretty=%s) 79 | TYPE=$(echo ${MSG} | awk '{{ print $1 }}') 80 | if ! [[ "${TYPE}" =~ ^(build|build\(deps\)|ci|docs|feat|fix|refactor|test|update):$ ]]; then 81 | EXIT=1 82 | echo "Commit message of commit ${commit} doesn't conform to 'type: msg' format:" 83 | echo "${MSG}" 84 | echo "-------------------------" 85 | fi 86 | done 87 | exit ${EXIT} 88 | -------------------------------------------------------------------------------- /.github/workflows/.reusable-docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | permissions: {} 4 | 5 | on: 6 | workflow_call: 7 | inputs: 8 | skip: 9 | description: "Want to skip running certain jobs 'none', 'non-required', 'all'?" 10 | type: string 11 | default: "none" 12 | 13 | jobs: 14 | docs: 15 | runs-on: ubuntu-latest 16 | if: | 17 | (github.actor != 'dependabot[bot]') && 18 | inputs.skip != 'all' 19 | permissions: 20 | contents: write 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 24 | with: 25 | fetch-depth: 0 26 | - name: Set release env 27 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 28 | - name: Configure the git user 29 | run: | 30 | git config user.name "versioning_user" 31 | git config user.email "semgr8s@securesystems.de" 32 | - name: Install python 33 | uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 34 | with: 35 | python-version-file: '.python-version' 36 | - name: Install poetry 37 | uses: snok/install-poetry@76e04a911780d5b312d89783f7b1cd627778900a # v1.4.1 38 | with: 39 | version: 1.8.3 40 | virtualenvs-create: false 41 | virtualenvs-in-project: false 42 | installer-parallel: true 43 | - name: Install dependencies 44 | run: | 45 | poetry install --only docs 46 | - name: Deploy 47 | if: inputs.skip != 'non-required' 48 | run: | 49 | if [[ "${GITHUB_REF}" == "refs/tags/v"* ]]; 50 | then 51 | mike deploy --push --update-aliases ${RELEASE_VERSION} latest 52 | elif [[ "${GITHUB_REF}" == "refs/heads/dev" ]]; then 53 | mike deploy --push ${RELEASE_VERSION} 54 | else 55 | mkdocs build 56 | fi 57 | -------------------------------------------------------------------------------- /.github/workflows/.reusable-sca.yml: -------------------------------------------------------------------------------- 1 | name: sca 2 | 3 | permissions: {} 4 | 5 | on: 6 | workflow_call: 7 | inputs: 8 | image: 9 | description: "Image used for testing, i.e. registry + repository + tag" 10 | type: string 11 | required: true 12 | registry: 13 | description: 'Registry to login to pull image, e.g. "ghcr.io" for GHCR, leave empty if image is public' 14 | type: string 15 | required: false 16 | default: '' 17 | repo_owner: 18 | description: 'Name of repository owner, e.g. "github.repository_owner" for ghcr.io' 19 | type: string 20 | required: false 21 | default: '' 22 | skip: 23 | description: "Want to skip running certain jobs 'none', 'non-required', 'all'?" 24 | type: string 25 | default: "none" 26 | output: 27 | description: 'Output either "sarif" (GITHUB_TOKEN with security-events:write) or print results as "table" and fail on error' 28 | type: string 29 | required: false 30 | default: 'sarif' 31 | 32 | jobs: 33 | trivy-image-scan: 34 | name: trivy image 35 | runs-on: ubuntu-latest 36 | if: inputs.skip != 'all' 37 | permissions: 38 | packages: read 39 | security-events: write 40 | container: 41 | image: docker:stable 42 | steps: 43 | - name: Checkout code 44 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 45 | - name: Run 46 | uses: ./.github/actions/trivy-image 47 | with: 48 | image: ${{ inputs.image }} 49 | registry: ${{ inputs.registry }} 50 | repo_owner: ${{ inputs.repo_owner }} 51 | repo_token: ${{ secrets.GITHUB_TOKEN }} 52 | output: ${{ inputs.output }} 53 | 54 | grype: 55 | name: grype 56 | runs-on: ubuntu-latest 57 | if: | 58 | inputs.skip != 'non-required' && 59 | inputs.skip != 'all' 60 | permissions: 61 | packages: read 62 | security-events: write 63 | container: 64 | image: docker:stable 65 | steps: 66 | - name: Checkout code 67 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 68 | - name: Run 69 | uses: ./.github/actions/grype 70 | with: 71 | image: ${{ inputs.image }} 72 | registry: ${{ inputs.registry }} 73 | repo_owner: ${{ inputs.repo_owner }} 74 | repo_token: ${{ secrets.GITHUB_TOKEN }} 75 | output: ${{ inputs.output }} 76 | 77 | # WIP: Syft issue seems to cause error (https://github.com/anchore/syft/issues/1622) 78 | dependency-submission: 79 | name: syft / dependency review 80 | runs-on: ubuntu-latest 81 | if: | 82 | inputs.skip != 'non-required' && 83 | inputs.skip != 'all' 84 | permissions: 85 | packages: read 86 | contents: write 87 | steps: 88 | - name: Login with registry 89 | if: inputs.registry != '' 90 | uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 91 | with: 92 | registry: ${{ inputs.registry }} 93 | username: ${{ inputs.repo_owner }} 94 | password: ${{ secrets.GITHUB_TOKEN }} 95 | - name: Run 96 | uses: anchore/sbom-action@df80a981bc6edbc4e220a492d3cbe9f5547a6e75 # v0.17.9 97 | with: 98 | image: ${{ inputs.image }} 99 | format: cyclonedx-json 100 | dependency-snapshot: ${{ inputs.output == 'sarif' }} 101 | -------------------------------------------------------------------------------- /.github/workflows/.reusable-unit-test.yml: -------------------------------------------------------------------------------- 1 | name: unit-test 2 | 3 | permissions: 4 | checks: write 5 | pull-requests: write 6 | 7 | on: 8 | workflow_call: 9 | inputs: 10 | skip: 11 | description: "Want to skip running certain jobs 'none', 'non-required', 'all'?" 12 | type: string 13 | default: "none" 14 | 15 | jobs: 16 | pytest: 17 | name: unit tests 18 | runs-on: ubuntu-latest 19 | if: inputs.skip != 'all' 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 23 | - name: Set up Docker buildx 24 | uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0 25 | - name: Build test image 26 | uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6.12.0 27 | with: 28 | push: false 29 | load: true 30 | cache-from: type=gha 31 | cache-to: type=gha,mode=max 32 | file: build/Dockerfile 33 | target: tester 34 | tags: semgr8s:tester 35 | provenance: false 36 | sbom: false 37 | - name: Test 38 | run: docker run --rm -t -v ${PWD}/tests/:/app/tests/ semgr8s:tester pytest --cov-report=term-missing:skip-covered --junitxml=tests/pytest.xml --cov=semgr8s tests/ | tee tests/pytest-coverage.txt 39 | - name: Coverage comment 40 | id: comment 41 | uses: MishaKav/pytest-coverage-comment@81882822c5b22af01f91bd3eacb1cefb6ad73dc2 # v1.1.53 42 | if: | 43 | github.event_name == 'pull_request' && 44 | inputs.skip != 'non-required' 45 | with: 46 | pytest-coverage-path: tests/pytest-coverage.txt 47 | junitxml-path: tests/pytest.xml 48 | - name: Publish Test Report 49 | uses: mikepenz/action-junit-report@62516aa379bff6370c95fd5894d5a27fb6619d9b # v5.2.0 50 | if: success() || failure() # always run even if the previous step fails 51 | with: 52 | report_paths: 'tests/pytest.xml' 53 | fail_on_failure: true 54 | 55 | semgr8s-rules-validate: 56 | name: validate rules 57 | runs-on: ubuntu-latest 58 | if: | 59 | inputs.skip != 'all' 60 | steps: 61 | - name: Checkout code 62 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 63 | - name: Install python 64 | uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 65 | with: 66 | python-version-file: '.python-version' 67 | - name: Install poetry 68 | uses: snok/install-poetry@76e04a911780d5b312d89783f7b1cd627778900a # v1.4.1 69 | with: 70 | version: 1.8.3 71 | virtualenvs-create: false 72 | virtualenvs-in-project: false 73 | installer-parallel: true 74 | - name: Install dependencies 75 | run: | 76 | poetry install --only main 77 | - name: Validate pre-configured rules 78 | run: | 79 | semgrep scan --metrics=off --validate --config ./charts/semgr8s/rules/ 80 | - name: Validate additional rules 81 | run: | 82 | semgrep scan --metrics=off --validate --config ./rules/ 83 | 84 | semgr8s-rules-test: 85 | name: test rules 86 | runs-on: ubuntu-latest 87 | if: | 88 | inputs.skip != 'all' 89 | steps: 90 | - name: Checkout code 91 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 92 | - name: Install python 93 | uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 94 | with: 95 | python-version-file: '.python-version' 96 | - name: Install poetry 97 | uses: snok/install-poetry@76e04a911780d5b312d89783f7b1cd627778900a # v1.4.1 98 | with: 99 | version: 1.8.3 100 | virtualenvs-create: false 101 | virtualenvs-in-project: false 102 | installer-parallel: true 103 | - name: Install dependencies 104 | run: | 105 | poetry install --only main 106 | - name: Test rules 107 | run: semgrep scan --metrics=off --test --config ./rules/ ./rules/tests/ 108 | 109 | -------------------------------------------------------------------------------- /.github/workflows/nightly-build.yml: -------------------------------------------------------------------------------- 1 | name: nightly-build 2 | 3 | permissions: {} 4 | 5 | on: 6 | schedule: 7 | - cron: "30 1 * * *" 8 | 9 | defaults: 10 | run: 11 | shell: bash 12 | 13 | jobs: 14 | ci: 15 | uses: ./.github/workflows/.reusable-ci.yml 16 | permissions: 17 | actions: read 18 | attestations: read 19 | checks: write 20 | contents: write 21 | deployments: read 22 | discussions: read 23 | id-token: write 24 | issues: read 25 | packages: write 26 | pages: read 27 | pull-requests: write 28 | repository-projects: read 29 | security-events: write 30 | statuses: read 31 | secrets: inherit 32 | with: 33 | skip_build: 'none' 34 | skip_compliance_checks: 'all' 35 | skip_unit_tests: 'non-required' 36 | skip_sast: 'all' 37 | skip_sca: 'none' 38 | skip_docs: 'all' 39 | skip_integration_tests: 'non-required' 40 | output_type: 'sarif' 41 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yaml: -------------------------------------------------------------------------------- 1 | name: nightly 2 | 3 | permissions: {} 4 | 5 | on: 6 | schedule: 7 | - cron: "30 1 * * *" 8 | 9 | defaults: 10 | run: 11 | shell: bash 12 | 13 | jobs: 14 | build: 15 | uses: ./.github/workflows/.reusable-build.yml 16 | permissions: 17 | id-token: write 18 | packages: write 19 | secrets: inherit 20 | with: 21 | skip: "non-required" 22 | 23 | compliance: 24 | uses: ./.github/workflows/.reusable-compliance.yml 25 | permissions: 26 | actions: read 27 | attestations: read 28 | checks: read 29 | contents: write 30 | deployments: read 31 | discussions: read 32 | id-token: write 33 | issues: read 34 | packages: read 35 | pages: read 36 | pull-requests: write 37 | repository-projects: read 38 | security-events: write 39 | statuses: read 40 | secrets: inherit 41 | with: 42 | skip: "none" 43 | 44 | sca-released: 45 | name: sca (released) 46 | uses: ./.github/workflows/.reusable-sca.yml 47 | needs: [build] 48 | permissions: 49 | contents: write 50 | security-events: write 51 | packages: read 52 | secrets: inherit 53 | with: 54 | image: ${{ needs.build.outputs.original_image }} 55 | skip: "none" 56 | output: "table" 57 | 58 | integration-test: 59 | uses: ./.github/workflows/.reusable-integration-test.yml 60 | needs: [build] 61 | permissions: 62 | packages: read 63 | secrets: inherit 64 | with: 65 | build_registry: ${{ needs.build.outputs.original_registry }} 66 | repo_owner: ${{ github.repository_owner }} 67 | build_image_repository: ${{ needs.build.outputs.original_registry }}/${{ needs.build.outputs.original_repo }} 68 | build_tag: ${{ needs.build.outputs.original_tag }} 69 | skip: "non-required" 70 | 71 | cleanup-registry: 72 | uses: ./.github/workflows/.reusable-cleanup-registry.yml 73 | needs: [build] 74 | secrets: inherit 75 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: pr 2 | 3 | permissions: {} 4 | 5 | on: 6 | pull_request: 7 | branches: 8 | - dev 9 | 10 | defaults: 11 | run: 12 | shell: bash 13 | 14 | jobs: 15 | ci: 16 | uses: ./.github/workflows/.reusable-ci.yml 17 | permissions: 18 | actions: read 19 | attestations: read 20 | checks: write 21 | contents: write 22 | deployments: read 23 | discussions: read 24 | id-token: write 25 | issues: read 26 | packages: write 27 | pages: read 28 | pull-requests: write 29 | repository-projects: read 30 | security-events: write 31 | statuses: read 32 | secrets: inherit 33 | with: 34 | skip_build: 'none' 35 | skip_compliance_checks: 'none' 36 | skip_unit_tests: 'none' 37 | skip_sast: 'none' 38 | skip_sca: 'non-required' 39 | skip_docs: 'non-required' 40 | skip_integration_tests: 'non-required' 41 | output_type: 'sarif' 42 | -------------------------------------------------------------------------------- /.github/workflows/pr2main.yml: -------------------------------------------------------------------------------- 1 | name: pr2main 2 | 3 | permissions: {} 4 | 5 | on: 6 | pull_request: 7 | branches: 8 | - main 9 | 10 | defaults: 11 | run: 12 | shell: bash 13 | 14 | jobs: 15 | ci: 16 | uses: ./.github/workflows/.reusable-ci.yml 17 | permissions: 18 | actions: read 19 | attestations: read 20 | checks: write 21 | contents: write 22 | deployments: read 23 | discussions: read 24 | id-token: write 25 | issues: read 26 | packages: write 27 | pages: read 28 | pull-requests: write 29 | repository-projects: read 30 | security-events: write 31 | statuses: read 32 | secrets: inherit 33 | with: 34 | skip_build: 'none' 35 | skip_compliance_checks: 'none' 36 | skip_unit_tests: 'none' 37 | skip_sast: 'none' 38 | skip_sca: 'none' 39 | skip_docs: 'non-required' 40 | skip_integration_tests: 'none' 41 | output_type: 'sarif' 42 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: push 2 | 3 | permissions: {} 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | - dev 10 | 11 | defaults: 12 | run: 13 | shell: bash 14 | 15 | jobs: 16 | ci: 17 | uses: ./.github/workflows/.reusable-ci.yml 18 | permissions: 19 | actions: read 20 | attestations: read 21 | checks: write 22 | contents: write 23 | deployments: read 24 | discussions: read 25 | id-token: write 26 | issues: read 27 | packages: write 28 | pages: read 29 | pull-requests: write 30 | repository-projects: read 31 | security-events: write 32 | statuses: read 33 | secrets: inherit 34 | with: 35 | skip_build: 'none' 36 | skip_compliance_checks: 'none' 37 | skip_unit_tests: 'non-required' 38 | skip_sast: 'none' 39 | skip_sca: 'none' 40 | skip_docs: 'none' 41 | skip_integration_tests: 'none' 42 | output_type: 'sarif' 43 | -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | name: Semgrep 2 | 3 | permissions: {} 4 | 5 | on: 6 | workflow_dispatch: {} 7 | workflow_call: {} 8 | 9 | jobs: 10 | semgrep: 11 | name: semgrep policy 12 | runs-on: ubuntu-latest 13 | if: | 14 | (github.event_name != 'push') && 15 | (github.actor != 'dependabot[bot]') 16 | env: 17 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 18 | container: 19 | image: semgrep/semgrep 20 | steps: 21 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | - run: semgrep ci 23 | -------------------------------------------------------------------------------- /.github/workflows/tag.yml: -------------------------------------------------------------------------------- 1 | name: tag 2 | 3 | permissions: {} 4 | 5 | on: 6 | push: 7 | tags: 8 | - "v*" 9 | 10 | defaults: 11 | run: 12 | shell: bash 13 | 14 | jobs: 15 | ci: 16 | uses: ./.github/workflows/.reusable-ci.yml 17 | permissions: 18 | actions: read 19 | attestations: read 20 | checks: write 21 | contents: write 22 | deployments: read 23 | discussions: read 24 | id-token: write 25 | issues: read 26 | packages: write 27 | pages: read 28 | pull-requests: write 29 | repository-projects: read 30 | security-events: write 31 | statuses: read 32 | secrets: inherit 33 | with: 34 | skip_build: 'none' 35 | skip_compliance_checks: 'none' 36 | skip_unit_tests: 'non-required' 37 | skip_sast: 'none' 38 | skip_sca: 'none' 39 | skip_docs: 'none' 40 | skip_integration_tests: 'non-required' 41 | output_type: 'sarif' 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | 3 | # project specific 4 | certs/* 5 | conf/* 6 | manifests/* 7 | !*/.gitkeep 8 | .pylintrc 9 | .demo 10 | __pycache__ 11 | -------------------------------------------------------------------------------- /.kube-linter/config.yaml: -------------------------------------------------------------------------------- 1 | checks: 2 | doNotAutoAddDefaults: false 3 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.12 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | webhookName := semgr8s 2 | image := $(shell yq e '.deployment.image.repository' charts/semgr8s/values.yaml) 3 | version := $(shell yq e '.appVersion' charts/semgr8s/Chart.yaml) 4 | tag := $(image):v$(version) 5 | 6 | ns := semgr8ns 7 | 8 | all: uninstall install 9 | 10 | .PHONY:build 11 | build: 12 | @echo "####################" 13 | @echo "## $(@)" 14 | @echo "####################" 15 | docker buildx build --platform=linux/amd64 -t $(tag) -f build/Dockerfile . 16 | 17 | .PHONY:build-tester 18 | build-tester: 19 | @echo "####################" 20 | @echo "## $(@)" 21 | @echo "####################" 22 | docker buildx build --platform=linux/amd64 --target tester -t $(tag)-tester -f build/Dockerfile . 23 | 24 | .PHONY:push 25 | push: 26 | @echo "####################" 27 | @echo "## $(@)" 28 | @echo "####################" 29 | docker push $(tag) 30 | 31 | .PHONY:install 32 | install: 33 | @echo "####################" 34 | @echo "## $(@)" 35 | @echo "####################" 36 | helm install semgr8s charts/semgr8s --atomic --create-namespace --namespace $(ns) 37 | 38 | .PHONY:uninstall 39 | uninstall: 40 | @echo "####################" 41 | @echo "## $(@)" 42 | @echo "####################" 43 | helm uninstall semgr8s -n $(ns) 44 | 45 | .PHONY:annihilate 46 | annihilate: 47 | @echo "####################" 48 | @echo "## $(@)" 49 | @echo "####################" 50 | helm uninstall semgr8s -n $(ns) 51 | kubectl delete ns $(ns) 52 | 53 | .PHONY: test 54 | test: 55 | @echo "####################" 56 | @echo "## $(@)" 57 | @echo "####################" 58 | -kubectl create -f tests/demo 59 | @echo 60 | -kubectl get pods -n test-semgr8s 61 | @echo 62 | -kubectl delete -f tests/demo 63 | 64 | .PHONY: unittest 65 | unittest: 66 | @echo "####################" 67 | @echo "## $(@)" 68 | @echo "####################" 69 | docker run --rm -t -v ${PWD}/tests/:/app/tests/ $(tag)-tester pytest --cov=semgr8s --cov-report term-missing tests/ 70 | -------------------------------------------------------------------------------- /build/Dockerfile: -------------------------------------------------------------------------------- 1 | ## BASE 2 | FROM alpine:3.20.3@sha256:beefdbd8a1da6d2915566fde36db9db0b524eb737fc57cd1367effd16dc0d06d as base 3 | 4 | COPY .python-version / 5 | RUN apk add --no-cache python3~="$(cat .python-version)" 6 | ARG POETRY_VERSION="1.8.3-r0" 7 | 8 | ## BUILDER 9 | FROM base as builder 10 | 11 | ENV POETRY_NO_INTERACTION=1 \ 12 | POETRY_VIRTUALENVS_IN_PROJECT=1 \ 13 | POETRY_VIRTUALENVS_CREATE=1 \ 14 | POETRY_CACHE_DIR=/tmp/poetry_cache 15 | 16 | RUN apk add --no-cache poetry="${POETRY_VERSION}" 17 | 18 | WORKDIR /app 19 | COPY pyproject.toml poetry.lock ./ 20 | 21 | RUN --mount=type=cache,target=$POETRY_CACHE_DIR poetry install --only main --no-root 22 | 23 | 24 | ## TESTER 25 | FROM base as tester 26 | 27 | ENV VIRTUAL_ENV=/app/.venv \ 28 | PATH="/app/.venv/bin:$PATH" \ 29 | POETRY_NO_INTERACTION=1 \ 30 | POETRY_VIRTUALENVS_IN_PROJECT=1 \ 31 | POETRY_VIRTUALENVS_CREATE=1 \ 32 | POETRY_CACHE_DIR=/tmp/poetry_cache 33 | 34 | WORKDIR /app 35 | RUN mkdir /app/rules && \ 36 | mkdir /app/data && \ 37 | mkdir /.cache && \ 38 | mkdir /.semgrep 39 | 40 | # Copy source code, packages, and rules 41 | COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV} 42 | COPY semgr8s/ /app/semgr8s 43 | COPY charts/semgr8s/rules/ /app/rules 44 | 45 | # Install test dependencies 46 | RUN apk add --no-cache poetry="${POETRY_VERSION}" 47 | COPY pyproject.toml poetry.lock ./ 48 | RUN --mount=type=cache,target=$POETRY_CACHE_DIR poetry install --only test 49 | 50 | CMD ["pytest", "--cov=semgr8s", "--cov-report=xml:tests/coverage.xml", "tests/"] 51 | 52 | 53 | 54 | ## SEMGR8S 55 | FROM base 56 | 57 | ENV VIRTUAL_ENV=/app/.venv \ 58 | PATH="/app/.venv/bin:$PATH" 59 | 60 | WORKDIR /app 61 | 62 | # Copy source code and packages 63 | COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV} 64 | COPY semgr8s/ /app/semgr8s 65 | COPY build/harden.sh / 66 | RUN sh /harden.sh 67 | 68 | USER 10001:20001 69 | 70 | LABEL org.opencontainers.image.documentation="https://semgr8ns.github.io/semgr8s/" 71 | LABEL org.opencontainers.image.authors="Christoph Hamsen " 72 | LABEL org.opencontainers.image.vendor="Secure Systems Engineering" 73 | 74 | CMD ["python", "-m", "semgr8s"] 75 | -------------------------------------------------------------------------------- /build/harden.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -euo pipefail 3 | 4 | # Update packages 5 | #apk update --no-cache && apk upgrade --no-cache && rm -rf /var/cache/apk 6 | 7 | # Remove apk 8 | find / -type f -iname '*apk*' -xdev -delete 9 | find / -type d -iname '*apk*' -print0 -xdev | xargs -0 rm -r -- 10 | 11 | # Remove pip 12 | pip uninstall pip --yes 13 | 14 | # Remove user accounts 15 | echo "" >/etc/group 16 | echo "" >/etc/passwd 17 | echo "" >/etc/shadow 18 | 19 | # Remove crons 20 | rm -fr /var/spool/cron 21 | rm -fr /etc/crontabs 22 | rm -fr /etc/periodic 23 | 24 | # Remove init scripts 25 | rm -fr /etc/init.d 26 | rm -fr /etc/conf.d 27 | rm -f /etc/inittab 28 | 29 | # Remove media stuff 30 | rm -f /etc/fstab 31 | rm -fr /media 32 | 33 | # Remove this file 34 | rm "$0" 35 | -------------------------------------------------------------------------------- /charts/semgr8s/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: semgr8s 3 | description: Semgrep-based Policy Controller for Kubernetes 4 | type: application 5 | version: "0.1.21" 6 | appVersion: "0.1.21" 7 | keywords: 8 | - kubernetes 9 | - admission controller 10 | - policy management 11 | home: https://semgr8ns.github.io/semgr8s/latest 12 | sources: 13 | - https://github.com/semgr8ns/semgr8s 14 | icon: https://raw.githubusercontent.com/semgr8ns/semgr8s/main/docs/assets/semgr8s-logo.png 15 | maintainers: 16 | - name: Christoph Hamsen 17 | email: christoph.hamsen@securesystems.de 18 | -------------------------------------------------------------------------------- /charts/semgr8s/rules/test-semgr8s-forbidden-label.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | - id: test-semgr8s-forbidden-label 3 | message: TEST ONLY. Found kubernetes resource with semgr8s forbidden test label. Any resource with label "semgr8s-test=forbidden-test-label-e3b0c44298fc1c" is denied. This label carries no meaning beyond testing and demonstration purposes. 4 | metadata: 5 | category: test 6 | technology: 7 | - kubernetes 8 | references: 9 | - https://semgr8ns.github.io/semgr8s/latest/#testing 10 | languages: [yaml] 11 | severity: INFO 12 | patterns: 13 | - pattern-inside: | 14 | metadata: 15 | ... 16 | - pattern-inside: | 17 | labels: 18 | ... 19 | - pattern: | 20 | semgr8s-test: forbidden-test-label-e3b0c44298fc1c 21 | fix: "" 22 | -------------------------------------------------------------------------------- /charts/semgr8s/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "semgr8s.name" -}} 5 | {{- .Chart.Name | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create chart name and version as used by the chart label. 10 | */}} 11 | {{- define "semgr8s.chart" -}} 12 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 13 | {{- end }} 14 | 15 | {{/* 16 | Common labels 17 | */}} 18 | {{- define "semgr8s.labels" -}} 19 | helm.sh/chart: {{ include "semgr8s.chart" . }} 20 | {{ include "semgr8s.selectorLabels" . }} 21 | {{- if .Chart.AppVersion }} 22 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 23 | {{- end }} 24 | app.kubernetes.io/managed-by: {{ .Release.Service }} 25 | {{- end }} 26 | 27 | {{/* 28 | Selector labels 29 | */}} 30 | {{- define "semgr8s.selectorLabels" -}} 31 | app.kubernetes.io/name: {{ include "semgr8s.name" . }} 32 | app.kubernetes.io/instance: {{ .Release.Name }} 33 | {{- end }} 34 | 35 | {{/* 36 | Create the name of the service to use 37 | */}} 38 | {{- define "semgr8s.serviceName" -}} 39 | {{- include "semgr8s.name" . }}-service 40 | {{- end }} 41 | 42 | {{/* 43 | Create the name of the webhook to use 44 | */}} 45 | {{- define "semgr8s.webhookName" -}} 46 | {{- include "semgr8s.name" . }}-webhook 47 | {{- end }} 48 | 49 | {{/* 50 | Create the name of the TLS secret to use 51 | */}} 52 | {{- define "semgr8s.TLSName" -}} 53 | {{- include "semgr8s.name" . }}-tls 54 | {{- end }} 55 | 56 | {{/* 57 | Create the name of the environments to use 58 | */}} 59 | {{- define "semgr8s.envName" -}} 60 | {{- include "semgr8s.name" . }}-env 61 | {{- end }} 62 | 63 | {{/* 64 | Create the name of the service account to use 65 | */}} 66 | {{- define "semgr8s.serviceAccountName" -}} 67 | {{- include "semgr8s.name" . }}-serviceaccount 68 | {{- end }} 69 | 70 | {{/* 71 | Create the name of the environments to use 72 | */}} 73 | {{- define "semgr8s.roleName" -}} 74 | {{- include "semgr8s.name" . }}-role 75 | {{- end }} 76 | 77 | {{/* 78 | Create the name of the environments to use 79 | */}} 80 | {{- define "semgr8s.roleBindingName" -}} 81 | {{- include "semgr8s.name" . }}-rolebinding 82 | {{- end }} 83 | 84 | {{- define "semgr8s.getFileName" -}} 85 | {{- . | trimPrefix "rules/" | replace "/" "_" }} 86 | {{- end }} 87 | -------------------------------------------------------------------------------- /charts/semgr8s/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "semgr8s.name" . }} 5 | labels: 6 | {{- include "semgr8s.labels" . | nindent 4 }} 7 | spec: 8 | replicas: {{ .Values.deployment.replicas }} 9 | selector: 10 | matchLabels: 11 | {{- include "semgr8s.selectorLabels" . | nindent 6 }} 12 | template: 13 | metadata: 14 | {{- with .Values.deployment.podAnnotations }} 15 | annotations: 16 | {{- toYaml . | nindent 8 }} 17 | {{- end }} 18 | labels: 19 | {{- include "semgr8s.selectorLabels" . | nindent 8 }} 20 | spec: 21 | serviceAccount: {{ include "semgr8s.serviceAccountName" . }} 22 | {{- with .Values.deployment.imagePullSecrets }} 23 | imagePullSecrets: 24 | {{- toYaml . | nindent 8 }} 25 | {{- end }} 26 | {{- with .Values.deployment.podSecurityContext }} 27 | securityContext: 28 | {{- toYaml . | nindent 8 }} 29 | {{- end }} 30 | containers: 31 | - name: {{ .Chart.Name }} 32 | {{- with .Values.deployment.securityContext }} 33 | securityContext: 34 | {{- toYaml . | nindent 12 }} 35 | {{- end }} 36 | image: "{{ .Values.deployment.image.repository }}:{{ .Values.deployment.image.tag | default (print "v" .Chart.AppVersion) }}" 37 | imagePullPolicy: {{ .Values.deployment.image.pullPolicy }} 38 | ports: 39 | - name: http 40 | containerPort: {{ .Values.deployment.containerPort }} 41 | resources: 42 | {{- toYaml .Values.deployment.resources | nindent 12 }} 43 | volumeMounts: 44 | - mountPath: /app/certs 45 | name: cert 46 | readOnly: true 47 | - mountPath: /.semgrep 48 | name: semgrep-config 49 | readOnly: false 50 | - mountPath: /.cache 51 | name: cache 52 | readOnly: false 53 | - mountPath: /tmp 54 | name: tmp 55 | readOnly: false 56 | - mountPath: /app/rules 57 | name: rules 58 | readOnly: false 59 | - mountPath: /app/data 60 | name: data 61 | readOnly: false 62 | envFrom: 63 | - configMapRef: 64 | name: {{ include "semgr8s.envName" . }} 65 | {{- if .Values.application.semgrepLogin }} 66 | env: 67 | - name: SEMGREP_APP_TOKEN 68 | valueFrom: 69 | secretKeyRef: 70 | name: semgrep-app-token 71 | key: token 72 | {{ end }} 73 | volumes: 74 | - name: cert 75 | secret: 76 | secretName: {{ include "semgr8s.TLSName" . }} 77 | - name: semgrep-config 78 | emptyDir: {} 79 | - name: cache 80 | emptyDir: {} 81 | - name: tmp 82 | emptyDir: 83 | medium: "Memory" 84 | sizeLimit: 10Mi 85 | - name: rules 86 | emptyDir: {} 87 | - name: data 88 | emptyDir: 89 | medium: "Memory" 90 | sizeLimit: 1Mi 91 | -------------------------------------------------------------------------------- /charts/semgr8s/templates/env.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ include "semgr8s.envName" . }} 5 | labels: 6 | {{- include "semgr8s.labels" . | nindent 4 }} 7 | data: 8 | ENFORCE: {{ .Values.application.enforce | quote }} 9 | LOG_LEVEL: {{.Values.application.logLevel | default "INFO"}} 10 | SEMGREP_RULES: {{ join " " .Values.application.remoteRules | quote }} 11 | NAMESPACE: {{ .Release.Namespace }} 12 | -------------------------------------------------------------------------------- /charts/semgr8s/templates/role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | name: {{ include "semgr8s.roleName" . }} 5 | labels: 6 | {{- include "semgr8s.labels" . | nindent 4 }} 7 | rules: 8 | - apiGroups: ["*"] 9 | resources: ["configmaps"] 10 | verbs: ["list", "get"] 11 | -------------------------------------------------------------------------------- /charts/semgr8s/templates/rolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: {{ include "semgr8s.roleBindingName" . }} 5 | labels: 6 | {{- include "semgr8s.labels" . | nindent 4 }} 7 | subjects: 8 | - kind: ServiceAccount 9 | name: {{ include "semgr8s.serviceAccountName" . }} 10 | namespace: {{ .Release.Namespace }} 11 | roleRef: 12 | kind: Role 13 | name: {{ include "semgr8s.roleName" . }} 14 | apiGroup: rbac.authorization.k8s.io -------------------------------------------------------------------------------- /charts/semgr8s/templates/rules.yaml: -------------------------------------------------------------------------------- 1 | 2 | {{ $currentScope := .}} 3 | {{ range $path, $_ := .Files.Glob "rules/*" }} 4 | apiVersion: v1 5 | kind: ConfigMap 6 | metadata: 7 | name: {{ include "semgr8s.getFileName" $path | trimSuffix ".yaml" }} 8 | labels: 9 | {{- include "semgr8s.labels" $currentScope | nindent 4 }} 10 | semgr8s/rule: "true" 11 | data: 12 | {{ include "semgr8s.getFileName" $path }}: | 13 | {{- $.Files.Get $path | nindent 4}} 14 | --- 15 | {{ end }} -------------------------------------------------------------------------------- /charts/semgr8s/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "semgr8s.serviceName" . }} 5 | labels: 6 | {{- include "semgr8s.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - port: {{ .Values.service.port }} 11 | targetPort: {{ .Values.deployment.containerPort }} 12 | selector: 13 | {{- include "semgr8s.selectorLabels" . | nindent 4 }} 14 | -------------------------------------------------------------------------------- /charts/semgr8s/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: {{ include "semgr8s.serviceAccountName" . }} 5 | labels: 6 | {{- include "semgr8s.labels" . | nindent 4 }} 7 | -------------------------------------------------------------------------------- /charts/semgr8s/templates/webhook.yaml: -------------------------------------------------------------------------------- 1 | {{- $svc := (include "semgr8s.serviceName" .) -}} 2 | {{- $altNames := list -}} 3 | {{- $altNames = append $altNames (printf "%s" $svc) -}} 4 | {{- $altNames = append $altNames (printf "%s.%s" $svc .Release.Namespace) -}} 5 | {{- $altNames = append $altNames (printf "%s.%s.svc" $svc .Release.Namespace) -}} 6 | {{- $altNames = append $altNames (printf "%s.%s.svc.cluster.local" $svc .Release.Namespace) -}} 7 | {{- $certificate := genSelfSignedCert (printf "%s.%s.svc" $svc .Release.Namespace) nil $altNames 36500 -}} 8 | 9 | apiVersion: v1 10 | kind: Secret 11 | metadata: 12 | name: {{ include "semgr8s.TLSName" . }} 13 | labels: 14 | {{- include "semgr8s.labels" . | nindent 4 }} 15 | type: Opaque 16 | data: 17 | tls.crt: {{ $certificate.Cert | b64enc }} 18 | tls.key: {{ $certificate.Key | b64enc }} 19 | --- 20 | apiVersion: admissionregistration.k8s.io/v1 21 | kind: ValidatingWebhookConfiguration 22 | metadata: 23 | name: {{ include "semgr8s.webhookName" . }} 24 | webhooks: 25 | - name: {{ .Chart.Name }}-svc.{{ .Release.Namespace }}.svc 26 | {{- with .Values.webhooks.validating }} 27 | {{- toYaml . | nindent 4 }} 28 | {{- end }} 29 | clientConfig: 30 | service: 31 | name: {{ include "semgr8s.serviceName" . }} 32 | namespace: {{ .Release.Namespace }} 33 | path: /validate/ 34 | caBundle: {{ $certificate.Cert | b64enc }} 35 | --- 36 | {{- if .Values.application.autofix }} 37 | apiVersion: admissionregistration.k8s.io/v1 38 | kind: MutatingWebhookConfiguration 39 | metadata: 40 | name: {{ include "semgr8s.webhookName" . }} 41 | webhooks: 42 | - name: {{ .Chart.Name }}-svc.{{ .Release.Namespace }}.svc 43 | {{- with .Values.webhooks.mutating }} 44 | {{- toYaml . | nindent 4 }} 45 | {{- end }} 46 | clientConfig: 47 | service: 48 | name: {{ include "semgr8s.serviceName" . }} 49 | namespace: {{ .Release.Namespace }} 50 | path: /mutate/ 51 | caBundle: {{ $certificate.Cert | b64enc }} 52 | {{ end }} 53 | -------------------------------------------------------------------------------- /charts/semgr8s/values.yaml: -------------------------------------------------------------------------------- 1 | deployment: 2 | image: 3 | repository: ghcr.io/semgr8ns/semgr8s 4 | pullPolicy: IfNotPresent 5 | tag: "" 6 | imagePullSecrets: [] 7 | replicas: 2 8 | containerPort: 5000 9 | podAnnotations: {} 10 | podSecurityContext: {} 11 | resources: 12 | limits: 13 | cpu: 1000m 14 | memory: 128Mi 15 | requests: 16 | cpu: 100m 17 | memory: 64Mi 18 | securityContext: 19 | allowPrivilegeEscalation: false 20 | capabilities: 21 | drop: 22 | - ALL 23 | privileged: false 24 | readOnlyRootFilesystem: true 25 | runAsNonRoot: true 26 | runAsUser: 10001 # remove when using openshift or OKD 4 27 | runAsGroup: 20001 # remove when using openshift or OKD 4 28 | seccompProfile: 29 | type: RuntimeDefault 30 | 31 | service: 32 | type: ClusterIP 33 | port: 443 34 | 35 | webhooks: # configuration options for webhooks described under https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#webhook-configuration 36 | validating: # main webhook 37 | failurePolicy: Fail 38 | sideEffects: None 39 | timeoutSeconds: 30 40 | admissionReviewVersions: ["v1","v1beta1"] 41 | namespaceSelector: 42 | matchLabels: 43 | semgr8s/validation: enabled 44 | rules: 45 | - scope: "Namespaced" 46 | apiGroups: ["", "apps", "batch", "networking.k8s.io", "rbac.authorization.k8s.io"] 47 | resources: ["*/*"] 48 | apiVersions: ["*"] 49 | operations: ["CREATE", "UPDATE"] 50 | mutating: # autofix webhook, only used when enabled 51 | failurePolicy: Fail 52 | sideEffects: None 53 | timeoutSeconds: 30 54 | admissionReviewVersions: ["v1","v1beta1"] 55 | namespaceSelector: 56 | matchLabels: 57 | semgr8s/validation: enabled 58 | rules: 59 | - scope: "Namespaced" 60 | apiGroups: ["", "apps", "batch", "networking.k8s.io", "rbac.authorization.k8s.io"] 61 | resources: ["*/*"] 62 | apiVersions: ["*"] 63 | operations: ["CREATE", "UPDATE"] 64 | 65 | application: 66 | # Configure the log level. Either one of `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`. Defaults to `INFO` 67 | logLevel: INFO 68 | # fail on rule violation (true/false) 69 | enforce: true 70 | # remoteRules: Apply remote rules from e.g. 71 | # * semgrep registry: https://semgrep.dev/r 72 | # * semgrep-rules github repo: https://github.com/semgrep/semgrep-rules 73 | # common choices: p/kubernetes, r/yaml.kubernetes 74 | remoteRules: ["p/kubernetes"] 75 | # apply semgrep fixes before validation (see https://semgrep.dev/docs/writing-rules/autofix) 76 | autofix: false 77 | # requires generic secret with name 'semgrep-app-token' and key 'token' in semgr8ns namespace 78 | semgrepLogin: false 79 | -------------------------------------------------------------------------------- /docs/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported versions 4 | 5 | While all known vulnerabilities in the Semgr8s application are listed below and we intent to fix vulnerabilities as soon as we become aware, both, Python and OS packages of the Semgr8s image may become vulnerable over time and we suggest to frequently update to the latest version or rebuilding the image from source yourself. 6 | At present, we only support the latest version. 7 | We stick to semantic versioning, so unless the major version changes, updating Semgr8s should never break your installation. 8 | 9 | ## Known vulnerabilities 10 | 11 | Known vulnerabilities are published after resolution under [GitHub Security](https://github.com/semgr8ns/semgr8s/security). 12 | 13 | ## Reporting a vulnerability 14 | 15 | We are very grateful for reports on vulnerabilities discovered in the project, specifically as it is intended to increase security for the community. 16 | We aim to investigate and fix these as soon as possible. Please submit vulnerabilities via [GitHub Vulnerability Reporting](https://github.com/semgr8ns/semgr8s/security/advisories/new). 17 | -------------------------------------------------------------------------------- /docs/assets/semgr8s-architecture-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semgr8ns/semgr8s/55d4e8d831e7e2bb3ef340f22f8b5172f88443e2/docs/assets/semgr8s-architecture-dark.png -------------------------------------------------------------------------------- /docs/assets/semgr8s-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semgr8ns/semgr8s/55d4e8d831e7e2bb3ef340f22f8b5172f88443e2/docs/assets/semgr8s-architecture.png -------------------------------------------------------------------------------- /docs/assets/semgr8s-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semgr8ns/semgr8s/55d4e8d831e7e2bb3ef340f22f8b5172f88443e2/docs/assets/semgr8s-demo.gif -------------------------------------------------------------------------------- /docs/assets/semgr8s-design-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semgr8ns/semgr8s/55d4e8d831e7e2bb3ef340f22f8b5172f88443e2/docs/assets/semgr8s-design-dark.png -------------------------------------------------------------------------------- /docs/assets/semgr8s-design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semgr8ns/semgr8s/55d4e8d831e7e2bb3ef340f22f8b5172f88443e2/docs/assets/semgr8s-design.png -------------------------------------------------------------------------------- /docs/assets/semgr8s-logo-full-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semgr8ns/semgr8s/55d4e8d831e7e2bb3ef340f22f8b5172f88443e2/docs/assets/semgr8s-logo-full-dark.png -------------------------------------------------------------------------------- /docs/assets/semgr8s-logo-full-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semgr8ns/semgr8s/55d4e8d831e7e2bb3ef340f22f8b5172f88443e2/docs/assets/semgr8s-logo-full-light.png -------------------------------------------------------------------------------- /docs/assets/semgr8s-logo-single.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semgr8ns/semgr8s/55d4e8d831e7e2bb3ef340f22f8b5172f88443e2/docs/assets/semgr8s-logo-single.png -------------------------------------------------------------------------------- /docs/assets/semgr8s-logo-vertical-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semgr8ns/semgr8s/55d4e8d831e7e2bb3ef340f22f8b5172f88443e2/docs/assets/semgr8s-logo-vertical-light.png -------------------------------------------------------------------------------- /docs/assets/semgr8s-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semgr8ns/semgr8s/55d4e8d831e7e2bb3ef340f22f8b5172f88443e2/docs/assets/semgr8s-logo.png -------------------------------------------------------------------------------- /docs/assets/sse-logo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /docs/assets/sse-logo-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 34 | 39 | 44 | 49 | 50 | -------------------------------------------------------------------------------- /docs/examples/deny-default-namespace.md: -------------------------------------------------------------------------------- 1 | # Deny `default` namespace 2 | 3 | Deny resources deployed to the default namespace. For granular security controls, resources should be segregated by namespace. 4 | 5 | ## Use rule 6 | 7 | In order to use this rule: 8 | 9 | 1. Create `configmap` via: 10 | ```bash 11 | kubectl create configmap -n semgr8ns deny-default-namespace --from-file=rules/deny-default-namespace.yaml 12 | kubectl label configmap -n semgr8ns deny-default-namespace semgr8s/rule=true 13 | ``` 14 | 15 | ## Rule 16 | 17 | ```yaml title="rules/deny-default-namespace.yaml" hl_lines="29" 18 | --8<-- "rules/deny-default-namespace.yaml" 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/examples/forbidden-namespaced-label.md: -------------------------------------------------------------------------------- 1 | # Namespaced rules 2 | 3 | Block resources with the forbidden test label in specific namespaces. The rule serves as an example to demonstrate how to restrict a rule to namespaces. 4 | 5 | ## Use rule 6 | 7 | In order to use this rule: 8 | 9 | 1. Adjust the label mapping to the target value. 10 | 2. Adjust metavariable regular expression for `$NS` to your target namespaces. 11 | 3. Create `configmap` via: 12 | ```bash 13 | kubectl create configmap -n semgr8ns forbidden-namespaced-label --from-file=rules/forbidden-namespaced-label.yaml 14 | kubectl label configmap -n semgr8ns forbidden-namespaced-label semgr8s/rule=true 15 | ``` 16 | 17 | ## Rule 18 | 19 | ```yaml title="rules/forbidden-namespaced-label.yaml" hl_lines="7-14" 20 | --8<-- "rules/forbidden-namespaced-label.yaml" 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/examples/forbidden-pod-label.md: -------------------------------------------------------------------------------- 1 | # Scoping to pods 2 | 3 | Block pods with the forbidden test label. The rule serves as an example to demonstrate how to restrict a rule to a specific resource type. 4 | 5 | ## Use rule 6 | 7 | In order to use this rule: 8 | 9 | 1. Adjust the label mapping to the target value. 10 | 2. Adjust `kind: Pod` mapping to your target resource type. 11 | 3. Create `configmap` via: 12 | ```bash 13 | kubectl create configmap -n semgr8ns forbidden-pod-label --from-file=rules/forbidden-pod-label.yaml 14 | kubectl label configmap -n semgr8ns forbidden-pod-label semgr8s/rule=true 15 | ``` 16 | 17 | ## Rule 18 | 19 | ```yaml title="rules/forbidden-pod-label.yaml" hl_lines="7-10" 20 | --8<-- "rules/forbidden-pod-label.yaml" 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/examples/forbidden-workload-label.md: -------------------------------------------------------------------------------- 1 | # Scoping to multiple resource types 2 | 3 | Block workloads with the forbidden test label. The rule serves as an example to demonstrate how to restrict a rule to a set of resource types. 4 | 5 | ## Use rule 6 | 7 | In order to use this rule: 8 | 9 | 1. Adjust the label mapping to the target value. 10 | 2. Adjust metavariable regular expression for `$KIND` to your target resource types. 11 | 3. Create `configmap` via: 12 | ```bash 13 | kubectl create configmap -n semgr8ns forbidden-workload-label --from-file=rules/forbidden-workload-label.yaml 14 | kubectl label configmap -n semgr8ns forbidden-workload-label semgr8s/rule=true 15 | ``` 16 | 17 | ## Rule 18 | 19 | ```yaml title="rules/forbidden-workload-label.yaml" hl_lines="7-13" 20 | --8<-- "rules/forbidden-workload-label.yaml" 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/examples/restrict-image-registry.md: -------------------------------------------------------------------------------- 1 | # Restrict image registry 2 | 3 | Restrict source registries and repositories for container images deployed to the cluster. Unauthorized container image sources can lead to supply chain attacks via targeted or accidental creation of malicious workloads. 4 | 5 | ## Use rule 6 | 7 | In order to use this rule: 8 | 9 | 1. Adjust metavariable-regex for `$IMG` in `rules/restrict-image-registry.yaml` (highlighted below) 10 | 2. Create `configmap` via: 11 | ```bash 12 | kubectl create configmap -n semgr8ns restrict-image-registry --from-file=rules/restrict-image-registry.yaml 13 | kubectl label configmap -n semgr8ns restrict-image-registry semgr8s/rule=true 14 | ``` 15 | 16 | ## Rule 17 | 18 | ```yaml title="rules/restrict-image-registry.yaml" hl_lines="29" 19 | --8<-- "rules/restrict-image-registry.yaml" 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/examples/template-autofix-rule.md: -------------------------------------------------------------------------------- 1 | # Template autofix rule 2 | 3 | Template rule demonstrating minimal syntax for [autofix rules](../usage.md#autofix) at the example of a forbidden test mapping that is removed upon fixing. 4 | 5 | ## Use rule 6 | 7 | !!! warning 8 | Not for practical use. 9 | Rule is only provided as a minimal example. 10 | 11 | In order to use this rule: 12 | 13 | 1. Create `configmap` via: 14 | ```bash 15 | kubectl create configmap -n semgr8ns template-autofix-rule --from-file=rules/template-autofix-rule.yaml 16 | kubectl label configmap -n semgr8ns template-autofix-rule semgr8s/rule=true 17 | ``` 18 | 19 | ## Rule 20 | 21 | ```yaml title="rules/template-autofix-rule.yaml" 22 | --8<-- "rules/template-autofix-rule.yaml" 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/examples/template-rule.md: -------------------------------------------------------------------------------- 1 | # Template rule 2 | 3 | Template rule demonstrating minimal syntax at the example of a forbidden test mapping. 4 | 5 | ## Use rule 6 | 7 | !!! warning 8 | Not for practical use. 9 | Rule is only provided as a minimal example. 10 | 11 | In order to use this rule: 12 | 13 | 1. Create `configmap` via: 14 | ```bash 15 | kubectl create configmap -n semgr8ns template-rule --from-file=rules/template-rule.yaml 16 | kubectl label configmap -n semgr8ns template-rule semgr8s/rule=true 17 | ``` 18 | 19 | ## Rule 20 | 21 | ```yaml title="rules/template-rule.yaml" 22 | --8<-- "rules/template-rule.yaml" 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/javascripts/tablesort.js: -------------------------------------------------------------------------------- 1 | document$.subscribe(function() { 2 | var tables = document.querySelectorAll("article table:not([class])") 3 | tables.forEach(function(table) { 4 | new Tablesort(table) 5 | }) 6 | }) 7 | 8 | -------------------------------------------------------------------------------- /docs/overrides/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block outdated %} 4 | You're not viewing the docs of the latest version. 5 | Click here to go to the latest version. 6 | {% endblock %} 7 | 8 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | # Project information 2 | site_name: Semgr8s - Semgrep-based Policy Controller for Kubernetes. 3 | 4 | site_url: https://semgr8ns.github.io/semgr8s/ 5 | site_description: >- 6 | Admission controller to use your well-known publicly available or custom Semgrep rules to validate k8s resources before deployment to the cluster. 7 | 8 | # Repository 9 | repo_name: semgr8ns/semgr8s/ 10 | repo_url: https://github.com/semgr8ns/semgr8s 11 | edit_uri: "" 12 | 13 | # Company 14 | copyright: Secure Systems Engineering GmbH 15 | 16 | 17 | # Configuration 18 | theme: 19 | language: en 20 | name: material 21 | custom_dir: docs/overrides 22 | palette: 23 | # Palette toggle for automatic mode 24 | - media: "(prefers-color-scheme)" 25 | primary: blue 26 | toggle: 27 | icon: material/brightness-auto 28 | name: system preference 29 | 30 | # Palette toggle for light mode 31 | - media: "(prefers-color-scheme: light)" 32 | scheme: default 33 | primary: blue 34 | toggle: 35 | icon: material/brightness-7 36 | name: light mode 37 | 38 | # Palette toggle for dark mode 39 | - media: "(prefers-color-scheme: dark)" 40 | scheme: slate 41 | primary: blue 42 | toggle: 43 | icon: material/brightness-4 44 | name: dark mode 45 | font: 46 | text: Roboto 47 | code: Roboto Mono 48 | logo: 'assets/semgr8s-logo-single.png' 49 | favicon: 'assets/semgr8s-logo-single.png' 50 | features: 51 | - content.code.annotate 52 | - content.code.copy 53 | - content.code.select 54 | - navigation.top 55 | - search.suggest 56 | - search.highlight 57 | 58 | # Extensions 59 | markdown_extensions: 60 | - admonition 61 | - codehilite 62 | - footnotes 63 | - attr_list 64 | - meta 65 | - pymdownx.details 66 | - pymdownx.emoji: 67 | emoji_index: !!python/name:material.extensions.emoji.twemoji 68 | emoji_generator: !!python/name:material.extensions.emoji.to_svg 69 | - pymdownx.highlight: 70 | anchor_linenums: true 71 | line_spans: __span 72 | pygments_lang_class: true 73 | - pymdownx.inlinehilite 74 | - pymdownx.snippets 75 | - pymdownx.superfences 76 | - pymdownx.tabbed 77 | - toc: 78 | permalink: ⚓︎ 79 | 80 | # Plugins 81 | plugins: 82 | - glightbox: 83 | skip_classes: 84 | - skip-lightbox 85 | - search 86 | 87 | # Extras 88 | extra_javascript: 89 | - https://unpkg.com/tablesort@5.3.0/dist/tablesort.min.js 90 | - javascripts/tablesort.js 91 | 92 | # Customization 93 | extra: 94 | version: 95 | provider: mike 96 | social: 97 | - icon: fontawesome/brands/github 98 | link: https://github.com/semgr8ns 99 | name: SSE on GitHub 100 | - icon: fontawesome/brands/docker 101 | link: https://ghcr.io/semgr8ns/semgr8s 102 | name: Semgr8s images on GHCR 103 | - icon: fontawesome/brands/medium 104 | link: https://medium.com/sse-blog 105 | name: SSE on Medium 106 | - icon: fontawesome/brands/youtube 107 | link: https://www.youtube.com/channel/UCReAmr98RzwYZeWG6CAwOhg 108 | name: SSE on YouTube 109 | - icon: fontawesome/brands/twitter 110 | link: https://twitter.com/sse_gmbh 111 | name: SSE on Twitter 112 | - icon: fontawesome/brands/linkedin 113 | link: https://www.linkedin.com/company/sse-secure-systems-engineering 114 | name: SSE on LinkedIn 115 | - icon: fontawesome/solid/link 116 | link: https://www.securesystems.de/ 117 | name: SSE Website 118 | - icon: fontawesome/solid/envelope 119 | link: mailto:semgr8s@securesystems.de 120 | name: Email contact 121 | 122 | # Page tree 123 | nav: 124 | - Intro: README.md 125 | - concept.md 126 | - usage.md 127 | - Exemplary rules: 128 | - examples/template-rule.md 129 | - examples/template-autofix-rule.md 130 | - examples/forbidden-pod-label.md 131 | - examples/forbidden-workload-label.md 132 | - examples/forbidden-namespaced-label.md 133 | - Selected rules: 134 | - examples/deny-default-namespace.md 135 | - examples/restrict-image-registry.md 136 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "semgr8s" 3 | version = "0.0.0" 4 | description = "Semgrep-based Policy controller for Kubernetes." 5 | authors = ["Christoph Hamsen "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.12" 10 | APScheduler = "3.11.0" 11 | Flask = "3.1.0" 12 | PyYAML = "6.0.2" 13 | semgrep = "1.104.0" 14 | jsonpatch = "1.33" 15 | cheroot = "10.0.1" 16 | 17 | [tool.poetry.group.docs] 18 | optional = false 19 | 20 | [tool.poetry.group.docs.dependencies] 21 | mkdocs-material = "9.5.50" 22 | mkdocs-glightbox = "0.4.0" 23 | mike = "2.1.3" 24 | 25 | [tool.poetry.group.dev] 26 | optional = false 27 | 28 | [tool.poetry.group.dev.dependencies] 29 | bandit = "1.8.2" 30 | bandit-sarif-formatter = "1.1.1" 31 | black = "24.10.0" 32 | pylint = "3.3.3" 33 | 34 | [tool.poetry.group.test] 35 | optional = false 36 | 37 | [tool.poetry.group.test.dependencies] 38 | pytest = "8.3.4" 39 | pytest-cov = "6.0.0" 40 | 41 | [build-system] 42 | requires = ["poetry-core"] 43 | build-backend = "poetry.core.masonry.api" 44 | -------------------------------------------------------------------------------- /rules/deny-default-namespace.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | - id: deny-default-namespace 3 | message: The default namespace should not be used. For granular security controls, resources should be segregated by namespace. 4 | metadata: 5 | likelihood: HIGH 6 | confidence: HIGH 7 | impact: LOW 8 | category: security 9 | technology: 10 | - kubernetes 11 | owasp: 12 | - A04:2021 - Insecure Design 13 | - K07:2022 - Network Segmentation 14 | references: 15 | - https://owasp.org/Top10/A04_2021-Insecure_Design/ 16 | - https://owasp.org/www-project-kubernetes-top-ten/2022/en/src/K07-network-segmentation 17 | languages: [yaml] 18 | severity: WARNING 19 | patterns: 20 | - pattern: | 21 | metadata: 22 | ... 23 | namespace: $NS 24 | - metavariable-regex: 25 | metavariable: $NS 26 | regex: (default) 27 | - focus-metavariable: $NS 28 | -------------------------------------------------------------------------------- /rules/forbidden-namespaced-label.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | - id: forbidden-namespaced-label 3 | message: Kubernetes resource with label forbidden in designated namespace. Any resource with label "semgr8s-test=forbidden-test-label-e3b0c44298fc1c" is denied for this namespace. This label carries no meaning beyond testing and demonstration purposes. 4 | languages: [yaml] 5 | severity: INFO 6 | patterns: 7 | - pattern-inside: | 8 | metadata: 9 | ... 10 | namespace: $NS 11 | ... 12 | - metavariable-regex: 13 | metavariable: $NS 14 | regex: (test-semgr8s|audit-semgr8s) 15 | # remaining pattern as normal 16 | - pattern-inside: | 17 | metadata: 18 | ... 19 | - pattern-inside: | 20 | labels: 21 | ... 22 | - pattern: | 23 | semgr8s-test: forbidden-test-label-e3b0c44298fc1c 24 | fix: "semgr8s-test: allowed-test-label" 25 | -------------------------------------------------------------------------------- /rules/forbidden-pod-label.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | - id: forbidden-pod-label 3 | message: Kubernetes pod with forbidden label. Any pod with label "semgr8s-test=forbidden-test-label-e3b0c44298fc1c" is denied. This label carries no meaning beyond testing and demonstration purposes. 4 | languages: [yaml] 5 | severity: INFO 6 | patterns: 7 | - pattern-inside: | 8 | ... 9 | kind: Pod 10 | ... 11 | # remaining pattern as normal 12 | - pattern-inside: | 13 | metadata: 14 | ... 15 | - pattern-inside: | 16 | labels: 17 | ... 18 | - pattern: | 19 | semgr8s-test: forbidden-test-label-e3b0c44298fc1c 20 | fix: "semgr8s-test: allowed-test-label" 21 | -------------------------------------------------------------------------------- /rules/forbidden-workload-label.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | - id: forbidden-workload-label 3 | message: Kubernetes workload with forbidden label. Any workload resource with label "semgr8s-test=forbidden-test-label-e3b0c44298fc1c" is denied. This label carries no meaning beyond testing and demonstration purposes. 4 | languages: [yaml] 5 | severity: INFO 6 | patterns: 7 | - pattern-inside: | 8 | ... 9 | kind: $KIND 10 | ... 11 | - metavariable-regex: 12 | metavariable: $KIND 13 | regex: (Pod|Deployment|ReplicaSet|DaemonSet|StatefulSet) 14 | # remaining pattern as normal 15 | - pattern-inside: | 16 | metadata: 17 | ... 18 | - pattern-inside: | 19 | labels: 20 | ... 21 | - pattern: | 22 | semgr8s-test: forbidden-test-label-e3b0c44298fc1c 23 | fix: "semgr8s-test: allowed-test-label" 24 | -------------------------------------------------------------------------------- /rules/restrict-image-registry.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | - id: restrict-image-registry 3 | message: Container image reference points to non-designated registry / repository. Referencing unauthorized container image sources can lead to supply chain attacks via targeted or accidental creation of malicious workloads. 4 | metadata: 5 | likelihood: HIGH 6 | confidence: HIGH 7 | impact: HIGH 8 | category: security 9 | technology: 10 | - kubernetes 11 | owasp: 12 | - A08:2021 - Software and Data Integrity Failures 13 | - K02:2022 - Supply Chain Vulnerabilities 14 | references: 15 | - https://owasp.org/Top10/A08_2021-Software_and_Data_Integrity_Failures/ 16 | - https://owasp.org/www-project-kubernetes-top-ten/2022/en/src/K02-supply-chain-vulnerabilities 17 | languages: [yaml] 18 | severity: ERROR 19 | patterns: 20 | - pattern-inside: | 21 | spec: 22 | ... 23 | - pattern-inside: | 24 | containers: 25 | ... 26 | - pattern: | 27 | image: $IMG 28 | - metavariable-regex: 29 | metavariable: $IMG 30 | regex: ^(?!docker\.io\/library\/).* # example for restriction to "docker.io/library/" 31 | - focus-metavariable: $IMG 32 | -------------------------------------------------------------------------------- /rules/template-autofix-rule.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | - id: template-autofix-rule 3 | message: Rule template with autofix. 4 | languages: [yaml] 5 | severity: INFO 6 | patterns: 7 | - pattern: | 8 | semgr8s-test: forbidden-test-label-e3b0c44298fc1c 9 | fix: "semgr8s-test: allowed-test-label" 10 | -------------------------------------------------------------------------------- /rules/template-rule.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | - id: template-rule 3 | message: Rule template. 4 | languages: [yaml] 5 | severity: INFO 6 | patterns: 7 | - pattern: | 8 | semgr8s-test: forbidden-test-label-e3b0c44298fc1c 9 | -------------------------------------------------------------------------------- /rules/tests/deny-default-namespace.test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: passing-testpod-1 6 | # ruleid: deny-default-namespace 7 | namespace: default 8 | labels: 9 | foo: bar 10 | spec: 11 | containers: 12 | - image: docker.io/library/busybox 13 | name: passing-testpod-1 14 | command: ["/bin/sh", "-ec", "sleep 1000"] 15 | securityContext: 16 | allowPrivilegeEscalation: false 17 | capabilities: 18 | drop: 19 | - ALL 20 | privileged: false 21 | readOnlyRootFilesystem: true 22 | runAsNonRoot: true 23 | runAsUser: 10001 24 | runAsGroup: 20001 25 | seccompProfile: 26 | type: RuntimeDefault 27 | --- 28 | apiVersion: v1 29 | kind: Pod 30 | metadata: 31 | name: passing-testpod-2 32 | # ok: deny-default-namespace 33 | namespace: test-semgr8s 34 | labels: 35 | foo: bar 36 | spec: 37 | containers: 38 | - image: docker.io/library/busybox 39 | name: passing-testpod-2 40 | command: ["/bin/sh", "-ec", "sleep 1000"] 41 | securityContext: 42 | allowPrivilegeEscalation: false 43 | capabilities: 44 | drop: 45 | - ALL 46 | privileged: false 47 | readOnlyRootFilesystem: true 48 | runAsNonRoot: true 49 | runAsUser: 10001 50 | runAsGroup: 20001 51 | seccompProfile: 52 | type: RuntimeDefault 53 | -------------------------------------------------------------------------------- /rules/tests/forbidden-namespaced-label.test.fixed.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: passing-testpod-1 6 | namespace: test-semgr8s 7 | labels: 8 | foo: bar 9 | # ruleid: forbidden-namespaced-label 10 | semgr8s-test: allowed-test-label 11 | spec: 12 | containers: 13 | - image: docker.io/library/busybox 14 | name: passing-testpod-1 15 | command: ["/bin/sh", "-ec", "sleep 1000"] 16 | securityContext: 17 | allowPrivilegeEscalation: false 18 | capabilities: 19 | drop: 20 | - ALL 21 | privileged: false 22 | readOnlyRootFilesystem: true 23 | runAsNonRoot: true 24 | runAsUser: 10001 25 | runAsGroup: 20001 26 | seccompProfile: 27 | type: RuntimeDefault 28 | --- 29 | apiVersion: v1 30 | kind: Pod 31 | metadata: 32 | name: passing-testpod-2 33 | namespace: test-otherns 34 | labels: 35 | foo: bar 36 | # ok: forbidden-namespaced-label 37 | semgr8s-test: forbidden-test-label-e3b0c44298fc1c 38 | spec: 39 | containers: 40 | - image: ghcr.io/library/busybox 41 | name: passing-testpod-2 42 | command: ["/bin/sh", "-ec", "sleep 1000"] 43 | securityContext: 44 | allowPrivilegeEscalation: false 45 | capabilities: 46 | drop: 47 | - ALL 48 | privileged: false 49 | readOnlyRootFilesystem: true 50 | runAsNonRoot: true 51 | runAsUser: 10001 52 | runAsGroup: 20001 53 | seccompProfile: 54 | type: RuntimeDefault 55 | --- 56 | apiVersion: v1 57 | kind: Pod 58 | metadata: 59 | name: passing-testpod-2 60 | labels: 61 | foo: bar 62 | # ok: forbidden-namespaced-label 63 | semgr8s-test: forbidden-test-label-e3b0c44298fc1c 64 | spec: 65 | containers: 66 | - image: ghcr.io/library/busybox 67 | name: passing-testpod-2 68 | command: ["/bin/sh", "-ec", "sleep 1000"] 69 | securityContext: 70 | allowPrivilegeEscalation: false 71 | capabilities: 72 | drop: 73 | - ALL 74 | privileged: false 75 | readOnlyRootFilesystem: true 76 | runAsNonRoot: true 77 | runAsUser: 10001 78 | runAsGroup: 20001 79 | seccompProfile: 80 | type: RuntimeDefault 81 | --- 82 | apiVersion: apps/v1 83 | kind: Deployment 84 | metadata: 85 | name: nginx-deployment-1 86 | namespace: test-semgr8s 87 | labels: 88 | app: nginx 89 | # ruleid: forbidden-namespaced-label 90 | semgr8s-test: allowed-test-label 91 | spec: 92 | replicas: 3 93 | selector: 94 | matchLabels: 95 | app: nginx 96 | template: 97 | metadata: 98 | labels: 99 | app: nginx 100 | spec: 101 | containers: 102 | - name: nginx 103 | image: nginx:1.14.2 104 | ports: 105 | - containerPort: 80 106 | --- 107 | apiVersion: apps/v1 108 | kind: Deployment 109 | metadata: 110 | name: nginx-deployment-2 111 | namespace: audit-semgr8s 112 | labels: 113 | app: nginx 114 | spec: 115 | replicas: 3 116 | selector: 117 | matchLabels: 118 | app: nginx 119 | template: 120 | metadata: 121 | labels: 122 | app: nginx 123 | # ruleid: forbidden-namespaced-label 124 | semgr8s-test: allowed-test-label 125 | spec: 126 | containers: 127 | - name: nginx 128 | image: docker.io/library/nginx:1.14.2 129 | ports: 130 | - containerPort: 80 131 | -------------------------------------------------------------------------------- /rules/tests/forbidden-namespaced-label.test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: passing-testpod-1 6 | namespace: test-semgr8s 7 | labels: 8 | foo: bar 9 | # ruleid: forbidden-namespaced-label 10 | semgr8s-test: forbidden-test-label-e3b0c44298fc1c 11 | spec: 12 | containers: 13 | - image: docker.io/library/busybox 14 | name: passing-testpod-1 15 | command: ["/bin/sh", "-ec", "sleep 1000"] 16 | securityContext: 17 | allowPrivilegeEscalation: false 18 | capabilities: 19 | drop: 20 | - ALL 21 | privileged: false 22 | readOnlyRootFilesystem: true 23 | runAsNonRoot: true 24 | runAsUser: 10001 25 | runAsGroup: 20001 26 | seccompProfile: 27 | type: RuntimeDefault 28 | --- 29 | apiVersion: v1 30 | kind: Pod 31 | metadata: 32 | name: passing-testpod-2 33 | namespace: test-otherns 34 | labels: 35 | foo: bar 36 | # ok: forbidden-namespaced-label 37 | semgr8s-test: forbidden-test-label-e3b0c44298fc1c 38 | spec: 39 | containers: 40 | - image: ghcr.io/library/busybox 41 | name: passing-testpod-2 42 | command: ["/bin/sh", "-ec", "sleep 1000"] 43 | securityContext: 44 | allowPrivilegeEscalation: false 45 | capabilities: 46 | drop: 47 | - ALL 48 | privileged: false 49 | readOnlyRootFilesystem: true 50 | runAsNonRoot: true 51 | runAsUser: 10001 52 | runAsGroup: 20001 53 | seccompProfile: 54 | type: RuntimeDefault 55 | --- 56 | apiVersion: v1 57 | kind: Pod 58 | metadata: 59 | name: passing-testpod-2 60 | labels: 61 | foo: bar 62 | # ok: forbidden-namespaced-label 63 | semgr8s-test: forbidden-test-label-e3b0c44298fc1c 64 | spec: 65 | containers: 66 | - image: ghcr.io/library/busybox 67 | name: passing-testpod-2 68 | command: ["/bin/sh", "-ec", "sleep 1000"] 69 | securityContext: 70 | allowPrivilegeEscalation: false 71 | capabilities: 72 | drop: 73 | - ALL 74 | privileged: false 75 | readOnlyRootFilesystem: true 76 | runAsNonRoot: true 77 | runAsUser: 10001 78 | runAsGroup: 20001 79 | seccompProfile: 80 | type: RuntimeDefault 81 | --- 82 | apiVersion: apps/v1 83 | kind: Deployment 84 | metadata: 85 | name: nginx-deployment-1 86 | namespace: test-semgr8s 87 | labels: 88 | app: nginx 89 | # ruleid: forbidden-namespaced-label 90 | semgr8s-test: forbidden-test-label-e3b0c44298fc1c 91 | spec: 92 | replicas: 3 93 | selector: 94 | matchLabels: 95 | app: nginx 96 | template: 97 | metadata: 98 | labels: 99 | app: nginx 100 | spec: 101 | containers: 102 | - name: nginx 103 | image: nginx:1.14.2 104 | ports: 105 | - containerPort: 80 106 | --- 107 | apiVersion: apps/v1 108 | kind: Deployment 109 | metadata: 110 | name: nginx-deployment-2 111 | namespace: audit-semgr8s 112 | labels: 113 | app: nginx 114 | spec: 115 | replicas: 3 116 | selector: 117 | matchLabels: 118 | app: nginx 119 | template: 120 | metadata: 121 | labels: 122 | app: nginx 123 | # ruleid: forbidden-namespaced-label 124 | semgr8s-test: forbidden-test-label-e3b0c44298fc1c 125 | spec: 126 | containers: 127 | - name: nginx 128 | image: docker.io/library/nginx:1.14.2 129 | ports: 130 | - containerPort: 80 131 | -------------------------------------------------------------------------------- /rules/tests/forbidden-pod-label.test.fixed.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: passing-testpod-1 6 | namespace: test-semgr8s 7 | labels: 8 | foo: bar 9 | # ruleid: forbidden-pod-label 10 | semgr8s-test: allowed-test-label 11 | spec: 12 | containers: 13 | - image: docker.io/library/busybox 14 | name: passing-testpod-1 15 | command: ["/bin/sh", "-ec", "sleep 1000"] 16 | securityContext: 17 | allowPrivilegeEscalation: false 18 | capabilities: 19 | drop: 20 | - ALL 21 | privileged: false 22 | readOnlyRootFilesystem: true 23 | runAsNonRoot: true 24 | runAsUser: 10001 25 | runAsGroup: 20001 26 | seccompProfile: 27 | type: RuntimeDefault 28 | --- 29 | apiVersion: v1 30 | kind: Pod 31 | metadata: 32 | name: passing-testpod-2 33 | namespace: test-semgr8s 34 | labels: 35 | foo: bar 36 | # ok: forbidden-pod-label 37 | semgr8s-test: foobar 38 | spec: 39 | containers: 40 | - image: ghcr.io/library/busybox 41 | name: passing-testpod-2 42 | command: ["/bin/sh", "-ec", "sleep 1000"] 43 | securityContext: 44 | allowPrivilegeEscalation: false 45 | capabilities: 46 | drop: 47 | - ALL 48 | privileged: false 49 | readOnlyRootFilesystem: true 50 | runAsNonRoot: true 51 | runAsUser: 10001 52 | runAsGroup: 20001 53 | seccompProfile: 54 | type: RuntimeDefault 55 | --- 56 | apiVersion: v1 57 | kind: Pod 58 | metadata: 59 | name: passing-testpod-2 60 | namespace: test-semgr8s 61 | labels: 62 | foo: bar 63 | # ok: forbidden-pod-label 64 | foobar: forbidden-test-label-e3b0c44298fc1c 65 | spec: 66 | containers: 67 | - image: ghcr.io/library/busybox 68 | name: passing-testpod-2 69 | command: ["/bin/sh", "-ec", "sleep 1000"] 70 | securityContext: 71 | allowPrivilegeEscalation: false 72 | capabilities: 73 | drop: 74 | - ALL 75 | privileged: false 76 | readOnlyRootFilesystem: true 77 | runAsNonRoot: true 78 | runAsUser: 10001 79 | runAsGroup: 20001 80 | seccompProfile: 81 | type: RuntimeDefault 82 | --- 83 | apiVersion: v1 84 | kind: Pod 85 | metadata: 86 | name: passing-testpod-3 87 | namespace: test-semgr8s 88 | # ok: forbidden-pod-label 89 | spec: 90 | containers: 91 | - image: docker.io/xoph/busybox 92 | name: passing-testpod-3 93 | command: ["/bin/sh", "-ec", "sleep 1000"] 94 | securityContext: 95 | allowPrivilegeEscalation: false 96 | capabilities: 97 | drop: 98 | - ALL 99 | privileged: false 100 | readOnlyRootFilesystem: true 101 | runAsNonRoot: true 102 | runAsUser: 10001 103 | runAsGroup: 20001 104 | seccompProfile: 105 | type: RuntimeDefault 106 | --- 107 | apiVersion: apps/v1 108 | kind: Deployment 109 | metadata: 110 | name: nginx-deployment-1 111 | labels: 112 | app: nginx 113 | # ok: forbidden-pod-label 114 | semgr8s-test: forbidden-test-label-e3b0c44298fc1c 115 | spec: 116 | replicas: 3 117 | selector: 118 | matchLabels: 119 | app: nginx 120 | template: 121 | metadata: 122 | labels: 123 | app: nginx 124 | spec: 125 | containers: 126 | - name: nginx 127 | image: nginx:1.14.2 128 | ports: 129 | - containerPort: 80 130 | --- 131 | apiVersion: apps/v1 132 | kind: Deployment 133 | metadata: 134 | name: nginx-deployment-2 135 | labels: 136 | app: nginx 137 | spec: 138 | replicas: 3 139 | selector: 140 | matchLabels: 141 | app: nginx 142 | template: 143 | metadata: 144 | labels: 145 | app: nginx 146 | # ok: forbidden-pod-label 147 | semgr8s-test: forbidden-test-label-e3b0c44298fc1c 148 | spec: 149 | containers: 150 | - name: nginx 151 | image: docker.io/library/nginx:1.14.2 152 | ports: 153 | - containerPort: 80 154 | -------------------------------------------------------------------------------- /rules/tests/forbidden-pod-label.test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: passing-testpod-1 6 | namespace: test-semgr8s 7 | labels: 8 | foo: bar 9 | # ruleid: forbidden-pod-label 10 | semgr8s-test: forbidden-test-label-e3b0c44298fc1c 11 | spec: 12 | containers: 13 | - image: docker.io/library/busybox 14 | name: passing-testpod-1 15 | command: ["/bin/sh", "-ec", "sleep 1000"] 16 | securityContext: 17 | allowPrivilegeEscalation: false 18 | capabilities: 19 | drop: 20 | - ALL 21 | privileged: false 22 | readOnlyRootFilesystem: true 23 | runAsNonRoot: true 24 | runAsUser: 10001 25 | runAsGroup: 20001 26 | seccompProfile: 27 | type: RuntimeDefault 28 | --- 29 | apiVersion: v1 30 | kind: Pod 31 | metadata: 32 | name: passing-testpod-2 33 | namespace: test-semgr8s 34 | labels: 35 | foo: bar 36 | # ok: forbidden-pod-label 37 | semgr8s-test: foobar 38 | spec: 39 | containers: 40 | - image: ghcr.io/library/busybox 41 | name: passing-testpod-2 42 | command: ["/bin/sh", "-ec", "sleep 1000"] 43 | securityContext: 44 | allowPrivilegeEscalation: false 45 | capabilities: 46 | drop: 47 | - ALL 48 | privileged: false 49 | readOnlyRootFilesystem: true 50 | runAsNonRoot: true 51 | runAsUser: 10001 52 | runAsGroup: 20001 53 | seccompProfile: 54 | type: RuntimeDefault 55 | --- 56 | apiVersion: v1 57 | kind: Pod 58 | metadata: 59 | name: passing-testpod-2 60 | namespace: test-semgr8s 61 | labels: 62 | foo: bar 63 | # ok: forbidden-pod-label 64 | foobar: forbidden-test-label-e3b0c44298fc1c 65 | spec: 66 | containers: 67 | - image: ghcr.io/library/busybox 68 | name: passing-testpod-2 69 | command: ["/bin/sh", "-ec", "sleep 1000"] 70 | securityContext: 71 | allowPrivilegeEscalation: false 72 | capabilities: 73 | drop: 74 | - ALL 75 | privileged: false 76 | readOnlyRootFilesystem: true 77 | runAsNonRoot: true 78 | runAsUser: 10001 79 | runAsGroup: 20001 80 | seccompProfile: 81 | type: RuntimeDefault 82 | --- 83 | apiVersion: v1 84 | kind: Pod 85 | metadata: 86 | name: passing-testpod-3 87 | namespace: test-semgr8s 88 | # ok: forbidden-pod-label 89 | spec: 90 | containers: 91 | - image: docker.io/xoph/busybox 92 | name: passing-testpod-3 93 | command: ["/bin/sh", "-ec", "sleep 1000"] 94 | securityContext: 95 | allowPrivilegeEscalation: false 96 | capabilities: 97 | drop: 98 | - ALL 99 | privileged: false 100 | readOnlyRootFilesystem: true 101 | runAsNonRoot: true 102 | runAsUser: 10001 103 | runAsGroup: 20001 104 | seccompProfile: 105 | type: RuntimeDefault 106 | --- 107 | apiVersion: apps/v1 108 | kind: Deployment 109 | metadata: 110 | name: nginx-deployment-1 111 | labels: 112 | app: nginx 113 | # ok: forbidden-pod-label 114 | semgr8s-test: forbidden-test-label-e3b0c44298fc1c 115 | spec: 116 | replicas: 3 117 | selector: 118 | matchLabels: 119 | app: nginx 120 | template: 121 | metadata: 122 | labels: 123 | app: nginx 124 | spec: 125 | containers: 126 | - name: nginx 127 | image: nginx:1.14.2 128 | ports: 129 | - containerPort: 80 130 | --- 131 | apiVersion: apps/v1 132 | kind: Deployment 133 | metadata: 134 | name: nginx-deployment-2 135 | labels: 136 | app: nginx 137 | spec: 138 | replicas: 3 139 | selector: 140 | matchLabels: 141 | app: nginx 142 | template: 143 | metadata: 144 | labels: 145 | app: nginx 146 | # ok: forbidden-pod-label 147 | semgr8s-test: forbidden-test-label-e3b0c44298fc1c 148 | spec: 149 | containers: 150 | - name: nginx 151 | image: docker.io/library/nginx:1.14.2 152 | ports: 153 | - containerPort: 80 154 | -------------------------------------------------------------------------------- /rules/tests/forbidden-workload-label.test.fixed.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: passing-testpod-1 6 | namespace: test-semgr8s 7 | labels: 8 | foo: bar 9 | # ruleid: forbidden-workload-label 10 | semgr8s-test: allowed-test-label 11 | spec: 12 | containers: 13 | - image: docker.io/library/busybox 14 | name: passing-testpod-1 15 | command: ["/bin/sh", "-ec", "sleep 1000"] 16 | securityContext: 17 | allowPrivilegeEscalation: false 18 | capabilities: 19 | drop: 20 | - ALL 21 | privileged: false 22 | readOnlyRootFilesystem: true 23 | runAsNonRoot: true 24 | runAsUser: 10001 25 | runAsGroup: 20001 26 | seccompProfile: 27 | type: RuntimeDefault 28 | --- 29 | apiVersion: v1 30 | kind: Pod 31 | metadata: 32 | name: passing-testpod-2 33 | namespace: test-semgr8s 34 | labels: 35 | foo: bar 36 | # ok: forbidden-workload-label 37 | semgr8s-test: foobar 38 | spec: 39 | containers: 40 | - image: ghcr.io/library/busybox 41 | name: passing-testpod-2 42 | command: ["/bin/sh", "-ec", "sleep 1000"] 43 | securityContext: 44 | allowPrivilegeEscalation: false 45 | capabilities: 46 | drop: 47 | - ALL 48 | privileged: false 49 | readOnlyRootFilesystem: true 50 | runAsNonRoot: true 51 | runAsUser: 10001 52 | runAsGroup: 20001 53 | seccompProfile: 54 | type: RuntimeDefault 55 | --- 56 | apiVersion: v1 57 | kind: Pod 58 | metadata: 59 | name: passing-testpod-2 60 | namespace: test-semgr8s 61 | labels: 62 | foo: bar 63 | # ok: forbidden-workload-label 64 | foobar: forbidden-test-label-e3b0c44298fc1c 65 | spec: 66 | containers: 67 | - image: ghcr.io/library/busybox 68 | name: passing-testpod-2 69 | command: ["/bin/sh", "-ec", "sleep 1000"] 70 | securityContext: 71 | allowPrivilegeEscalation: false 72 | capabilities: 73 | drop: 74 | - ALL 75 | privileged: false 76 | readOnlyRootFilesystem: true 77 | runAsNonRoot: true 78 | runAsUser: 10001 79 | runAsGroup: 20001 80 | seccompProfile: 81 | type: RuntimeDefault 82 | --- 83 | apiVersion: v1 84 | kind: Pod 85 | metadata: 86 | name: passing-testpod-3 87 | namespace: test-semgr8s 88 | labels: 89 | # ok: forbidden-workload-label 90 | foo: bar 91 | spec: 92 | containers: 93 | - image: docker.io/xoph/busybox 94 | name: passing-testpod-3 95 | command: ["/bin/sh", "-ec", "sleep 1000"] 96 | securityContext: 97 | allowPrivilegeEscalation: false 98 | capabilities: 99 | drop: 100 | - ALL 101 | privileged: false 102 | readOnlyRootFilesystem: true 103 | runAsNonRoot: true 104 | runAsUser: 10001 105 | runAsGroup: 20001 106 | seccompProfile: 107 | type: RuntimeDefault 108 | --- 109 | apiVersion: v1 110 | kind: Pod 111 | metadata: 112 | name: passing-testpod-3 113 | namespace: test-semgr8s 114 | spec: 115 | containers: 116 | - image: docker.io/xoph/busybox 117 | name: passing-testpod-3 118 | command: ["/bin/sh", "-ec", "sleep 1000"] 119 | securityContext: 120 | allowPrivilegeEscalation: false 121 | capabilities: 122 | drop: 123 | - ALL 124 | privileged: false 125 | readOnlyRootFilesystem: true 126 | runAsNonRoot: true 127 | runAsUser: 10001 128 | runAsGroup: 20001 129 | seccompProfile: 130 | type: RuntimeDefault 131 | --- 132 | apiVersion: apps/v1 133 | kind: Deployment 134 | metadata: 135 | name: nginx-deployment-1 136 | labels: 137 | app: nginx 138 | # ruleid: forbidden-workload-label 139 | semgr8s-test: allowed-test-label 140 | spec: 141 | replicas: 3 142 | selector: 143 | matchLabels: 144 | app: nginx 145 | template: 146 | metadata: 147 | labels: 148 | app: nginx 149 | spec: 150 | containers: 151 | - name: nginx 152 | image: nginx:1.14.2 153 | ports: 154 | - containerPort: 80 155 | --- 156 | apiVersion: apps/v1 157 | kind: Deployment 158 | metadata: 159 | name: nginx-deployment-2 160 | labels: 161 | app: nginx 162 | spec: 163 | replicas: 3 164 | selector: 165 | matchLabels: 166 | app: nginx 167 | template: 168 | metadata: 169 | labels: 170 | app: nginx 171 | # ruleid: forbidden-workload-label 172 | semgr8s-test: allowed-test-label 173 | spec: 174 | containers: 175 | - name: nginx 176 | image: docker.io/library/nginx:1.14.2 177 | ports: 178 | - containerPort: 80 179 | -------------------------------------------------------------------------------- /rules/tests/forbidden-workload-label.test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: passing-testpod-1 6 | namespace: test-semgr8s 7 | labels: 8 | foo: bar 9 | # ruleid: forbidden-workload-label 10 | semgr8s-test: forbidden-test-label-e3b0c44298fc1c 11 | spec: 12 | containers: 13 | - image: docker.io/library/busybox 14 | name: passing-testpod-1 15 | command: ["/bin/sh", "-ec", "sleep 1000"] 16 | securityContext: 17 | allowPrivilegeEscalation: false 18 | capabilities: 19 | drop: 20 | - ALL 21 | privileged: false 22 | readOnlyRootFilesystem: true 23 | runAsNonRoot: true 24 | runAsUser: 10001 25 | runAsGroup: 20001 26 | seccompProfile: 27 | type: RuntimeDefault 28 | --- 29 | apiVersion: v1 30 | kind: Pod 31 | metadata: 32 | name: passing-testpod-2 33 | namespace: test-semgr8s 34 | labels: 35 | foo: bar 36 | # ok: forbidden-workload-label 37 | semgr8s-test: foobar 38 | spec: 39 | containers: 40 | - image: ghcr.io/library/busybox 41 | name: passing-testpod-2 42 | command: ["/bin/sh", "-ec", "sleep 1000"] 43 | securityContext: 44 | allowPrivilegeEscalation: false 45 | capabilities: 46 | drop: 47 | - ALL 48 | privileged: false 49 | readOnlyRootFilesystem: true 50 | runAsNonRoot: true 51 | runAsUser: 10001 52 | runAsGroup: 20001 53 | seccompProfile: 54 | type: RuntimeDefault 55 | --- 56 | apiVersion: v1 57 | kind: Pod 58 | metadata: 59 | name: passing-testpod-2 60 | namespace: test-semgr8s 61 | labels: 62 | foo: bar 63 | # ok: forbidden-workload-label 64 | foobar: forbidden-test-label-e3b0c44298fc1c 65 | spec: 66 | containers: 67 | - image: ghcr.io/library/busybox 68 | name: passing-testpod-2 69 | command: ["/bin/sh", "-ec", "sleep 1000"] 70 | securityContext: 71 | allowPrivilegeEscalation: false 72 | capabilities: 73 | drop: 74 | - ALL 75 | privileged: false 76 | readOnlyRootFilesystem: true 77 | runAsNonRoot: true 78 | runAsUser: 10001 79 | runAsGroup: 20001 80 | seccompProfile: 81 | type: RuntimeDefault 82 | --- 83 | apiVersion: v1 84 | kind: Pod 85 | metadata: 86 | name: passing-testpod-3 87 | namespace: test-semgr8s 88 | labels: 89 | # ok: forbidden-workload-label 90 | foo: bar 91 | spec: 92 | containers: 93 | - image: docker.io/xoph/busybox 94 | name: passing-testpod-3 95 | command: ["/bin/sh", "-ec", "sleep 1000"] 96 | securityContext: 97 | allowPrivilegeEscalation: false 98 | capabilities: 99 | drop: 100 | - ALL 101 | privileged: false 102 | readOnlyRootFilesystem: true 103 | runAsNonRoot: true 104 | runAsUser: 10001 105 | runAsGroup: 20001 106 | seccompProfile: 107 | type: RuntimeDefault 108 | --- 109 | apiVersion: v1 110 | kind: Pod 111 | metadata: 112 | name: passing-testpod-3 113 | namespace: test-semgr8s 114 | spec: 115 | containers: 116 | - image: docker.io/xoph/busybox 117 | name: passing-testpod-3 118 | command: ["/bin/sh", "-ec", "sleep 1000"] 119 | securityContext: 120 | allowPrivilegeEscalation: false 121 | capabilities: 122 | drop: 123 | - ALL 124 | privileged: false 125 | readOnlyRootFilesystem: true 126 | runAsNonRoot: true 127 | runAsUser: 10001 128 | runAsGroup: 20001 129 | seccompProfile: 130 | type: RuntimeDefault 131 | --- 132 | apiVersion: apps/v1 133 | kind: Deployment 134 | metadata: 135 | name: nginx-deployment-1 136 | labels: 137 | app: nginx 138 | # ruleid: forbidden-workload-label 139 | semgr8s-test: forbidden-test-label-e3b0c44298fc1c 140 | spec: 141 | replicas: 3 142 | selector: 143 | matchLabels: 144 | app: nginx 145 | template: 146 | metadata: 147 | labels: 148 | app: nginx 149 | spec: 150 | containers: 151 | - name: nginx 152 | image: nginx:1.14.2 153 | ports: 154 | - containerPort: 80 155 | --- 156 | apiVersion: apps/v1 157 | kind: Deployment 158 | metadata: 159 | name: nginx-deployment-2 160 | labels: 161 | app: nginx 162 | spec: 163 | replicas: 3 164 | selector: 165 | matchLabels: 166 | app: nginx 167 | template: 168 | metadata: 169 | labels: 170 | app: nginx 171 | # ruleid: forbidden-workload-label 172 | semgr8s-test: forbidden-test-label-e3b0c44298fc1c 173 | spec: 174 | containers: 175 | - name: nginx 176 | image: docker.io/library/nginx:1.14.2 177 | ports: 178 | - containerPort: 80 179 | -------------------------------------------------------------------------------- /rules/tests/restrict-image-registry.test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: passing-testpod-1 6 | namespace: test-semgr8s 7 | spec: 8 | containers: 9 | # ok: restrict-image-registry 10 | - image: docker.io/library/busybox 11 | name: passing-testpod-1 12 | command: ["/bin/sh", "-ec", "sleep 1000"] 13 | securityContext: 14 | allowPrivilegeEscalation: false 15 | capabilities: 16 | drop: 17 | - ALL 18 | privileged: false 19 | readOnlyRootFilesystem: true 20 | runAsNonRoot: true 21 | runAsUser: 10001 22 | runAsGroup: 20001 23 | seccompProfile: 24 | type: RuntimeDefault 25 | --- 26 | apiVersion: v1 27 | kind: Pod 28 | metadata: 29 | name: passing-testpod-2 30 | namespace: test-semgr8s 31 | spec: 32 | containers: 33 | # ruleid: restrict-image-registry 34 | - image: ghcr.io/library/busybox 35 | name: passing-testpod-2 36 | command: ["/bin/sh", "-ec", "sleep 1000"] 37 | securityContext: 38 | allowPrivilegeEscalation: false 39 | capabilities: 40 | drop: 41 | - ALL 42 | privileged: false 43 | readOnlyRootFilesystem: true 44 | runAsNonRoot: true 45 | runAsUser: 10001 46 | runAsGroup: 20001 47 | seccompProfile: 48 | type: RuntimeDefault 49 | --- 50 | apiVersion: v1 51 | kind: Pod 52 | metadata: 53 | name: passing-testpod-3 54 | namespace: test-semgr8s 55 | spec: 56 | containers: 57 | # ruleid: restrict-image-registry 58 | - image: docker.io/xoph/busybox 59 | name: passing-testpod-3 60 | command: ["/bin/sh", "-ec", "sleep 1000"] 61 | securityContext: 62 | allowPrivilegeEscalation: false 63 | capabilities: 64 | drop: 65 | - ALL 66 | privileged: false 67 | readOnlyRootFilesystem: true 68 | runAsNonRoot: true 69 | runAsUser: 10001 70 | runAsGroup: 20001 71 | seccompProfile: 72 | type: RuntimeDefault 73 | --- 74 | apiVersion: apps/v1 75 | kind: Deployment 76 | metadata: 77 | name: nginx-deployment-1 78 | labels: 79 | app: nginx 80 | spec: 81 | replicas: 3 82 | selector: 83 | matchLabels: 84 | app: nginx 85 | template: 86 | metadata: 87 | labels: 88 | app: nginx 89 | spec: 90 | containers: 91 | - name: nginx 92 | # ruleid: restrict-image-registry 93 | image: nginx:1.14.2 94 | ports: 95 | - containerPort: 80 96 | --- 97 | apiVersion: apps/v1 98 | kind: Deployment 99 | metadata: 100 | name: nginx-deployment-2 101 | labels: 102 | app: nginx 103 | spec: 104 | replicas: 3 105 | selector: 106 | matchLabels: 107 | app: nginx 108 | template: 109 | metadata: 110 | labels: 111 | app: nginx 112 | spec: 113 | containers: 114 | - name: nginx 115 | # ok: restrict-image-registry 116 | image: docker.io/library/nginx:1.14.2 117 | ports: 118 | - containerPort: 80 119 | -------------------------------------------------------------------------------- /rules/tests/template-autofix-rule.test.fixed.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: passing-testpod-1 6 | namespace: test-semgr8s 7 | labels: 8 | foo: bar 9 | # ruleid: template-autofix-rule 10 | semgr8s-test: allowed-test-label 11 | spec: 12 | containers: 13 | - image: docker.io/library/busybox 14 | name: passing-testpod-1 15 | command: ["/bin/sh", "-ec", "sleep 1000"] 16 | --- 17 | apiVersion: v1 18 | kind: Pod 19 | metadata: 20 | name: passing-testpod-2 21 | namespace: test-semgr8s 22 | labels: 23 | foo: bar 24 | # ok: template-autofix-rule 25 | semgr8s-test: foobar 26 | spec: 27 | containers: 28 | - image: ghcr.io/library/busybox 29 | name: passing-testpod-2 30 | command: ["/bin/sh", "-ec", "sleep 1000"] 31 | -------------------------------------------------------------------------------- /rules/tests/template-autofix-rule.test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: passing-testpod-1 6 | namespace: test-semgr8s 7 | labels: 8 | foo: bar 9 | # ruleid: template-autofix-rule 10 | semgr8s-test: forbidden-test-label-e3b0c44298fc1c 11 | spec: 12 | containers: 13 | - image: docker.io/library/busybox 14 | name: passing-testpod-1 15 | command: ["/bin/sh", "-ec", "sleep 1000"] 16 | --- 17 | apiVersion: v1 18 | kind: Pod 19 | metadata: 20 | name: passing-testpod-2 21 | namespace: test-semgr8s 22 | labels: 23 | foo: bar 24 | # ok: template-autofix-rule 25 | semgr8s-test: foobar 26 | spec: 27 | containers: 28 | - image: ghcr.io/library/busybox 29 | name: passing-testpod-2 30 | command: ["/bin/sh", "-ec", "sleep 1000"] 31 | -------------------------------------------------------------------------------- /rules/tests/template-rule.test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: passing-testpod-1 6 | namespace: test-semgr8s 7 | labels: 8 | foo: bar 9 | # ruleid: template-rule 10 | semgr8s-test: forbidden-test-label-e3b0c44298fc1c 11 | spec: 12 | containers: 13 | - image: docker.io/library/busybox 14 | name: passing-testpod-1 15 | command: ["/bin/sh", "-ec", "sleep 1000"] 16 | --- 17 | apiVersion: v1 18 | kind: Pod 19 | metadata: 20 | name: passing-testpod-2 21 | namespace: test-semgr8s 22 | labels: 23 | foo: bar 24 | # ok: template-rule 25 | semgr8s-test: foobar 26 | spec: 27 | containers: 28 | - image: ghcr.io/library/busybox 29 | name: passing-testpod-2 30 | command: ["/bin/sh", "-ec", "sleep 1000"] 31 | -------------------------------------------------------------------------------- /semgr8s/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semgr8ns/semgr8s/55d4e8d831e7e2bb3ef340f22f8b5172f88443e2/semgr8s/__init__.py -------------------------------------------------------------------------------- /semgr8s/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Main method starting the web server. 3 | """ 4 | 5 | import logging 6 | import os 7 | 8 | from apscheduler.schedulers.background import BackgroundScheduler 9 | from cheroot.server import HTTPServer 10 | from cheroot.wsgi import Server 11 | from cheroot.ssl.builtin import BuiltinSSLAdapter 12 | 13 | from semgr8s.app import APP 14 | from semgr8s.updater import update_rules 15 | 16 | 17 | if __name__ == "__main__": 18 | LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO") 19 | APP.logger.setLevel(logging.getLevelName(LOG_LEVEL)) 20 | 21 | scheduler = BackgroundScheduler() 22 | job = scheduler.add_job(update_rules, "interval", minutes=1) 23 | scheduler.start() 24 | 25 | # first run at start up 26 | update_rules() 27 | 28 | HTTPServer.ssl_adapter = BuiltinSSLAdapter( 29 | certificate="/app/certs/tls.crt", private_key="/app/certs/tls.key" 30 | ) 31 | server = Server(("0.0.0.0", 5000), APP) 32 | server.start() 33 | -------------------------------------------------------------------------------- /semgr8s/app.py: -------------------------------------------------------------------------------- 1 | """ 2 | Flask application to expose required paths and generate suitable responses. 3 | """ 4 | 5 | import base64 6 | import json 7 | import os 8 | import sys 9 | 10 | import jsonpatch 11 | from flask import Flask, jsonify, request 12 | from semgrep.cli import cli 13 | from werkzeug.exceptions import UnsupportedMediaType 14 | 15 | from semgr8s.files import S8sFiles 16 | 17 | APP = Flask(__name__) 18 | AUDIT = os.environ.get("ENFORCE", "true").lower() == "false" 19 | 20 | 21 | @APP.route("/health", methods=["GET", "POST"]) 22 | def healthz(): 23 | """ 24 | Handle the '/health' endpoint and check the health status of the web server. 25 | Return '200' status code. 26 | """ 27 | return "", 200 28 | 29 | 30 | @APP.route("/ready", methods=["GET", "POST"]) 31 | def readyz(): 32 | """ 33 | Handle the '/ready' endpoint and check the readiness of the web server. 34 | Return '200' status code. 35 | """ 36 | return "", 200 37 | 38 | 39 | @APP.route("/validate", methods=["POST"]) 40 | def validate(): 41 | """ 42 | Handle the '/validate' endpoint and accept admission requests. 43 | Return response allowing or denying the request. 44 | """ 45 | 46 | return perform_review(request, autofix=False) 47 | 48 | 49 | @APP.route("/mutate", methods=["POST"]) 50 | def mutate(): 51 | """ 52 | Handle the '/mutate' endpoint and accept admission requests. 53 | Return response allowing, denying, or modifying the request. 54 | """ 55 | 56 | return perform_review(request, autofix=True) 57 | 58 | 59 | def perform_review( 60 | admission_request, autofix=False 61 | ): # pylint: disable=too-many-return-statements,too-many-branches 62 | """ 63 | Review admission request using semgrep as policy engine. 64 | Return admission response. 65 | 66 | Return '200' status code with response: 67 | { 68 | 'apiVersion': 'admission.k8s.io/v1', 69 | 'kind': 'AdmissionReview', 70 | 'response': { 71 | 'allowed': , 72 | 'status': { 73 | 'code': , 74 | 'msg': 75 | }, 76 | 'uid': 77 | } 78 | } 79 | 80 | Response payload status codes: 81 | * 201 - Admitted 82 | * 202 - Passed mutation phase, validation still open 83 | * 400 - Malformed request payload lacking 'request' key/values 84 | * 403 - Request violates policy rules 85 | * 406 - K8s resource 'event' ignored 86 | * 415 - Unsupported request payload media type 87 | * 418 - Error during semgrep scan 88 | * 422 - Malformed request payload without 'request.uid' key 89 | * 500 - Unexpected Webhook exception 90 | """ 91 | uid = "" 92 | files = S8sFiles() 93 | 94 | try: 95 | APP.logger.debug("+ request object: %s", admission_request) 96 | try: 97 | req = (admission_request.get_json()).get("request", {}) 98 | except (UnsupportedMediaType, AttributeError) as err: 99 | return send_response(False, "none", 415, f"Unsupported Media Type: {err}") 100 | 101 | APP.logger.debug("+ request: %s", req) 102 | 103 | if not req: 104 | return send_response( 105 | False, "none", 400, "Malformed request, no payload.request found" 106 | ) 107 | 108 | uid = req.get("uid", "") 109 | if not uid: 110 | return send_response( 111 | False, "none", 422, "Malformed request, no payload.request.uid found" 112 | ) 113 | 114 | kind = req.get("kind", {}).get("kind", "") 115 | APP.logger.debug("+ k8s resource kind: %s", kind) 116 | if not kind or kind == "Event": 117 | return send_response(True, uid, 406, f"Ignored k8s resource kind '{kind}'") 118 | 119 | k8s_res = req.get("object", {}) 120 | files.write_k8s_yaml(data=k8s_res) 121 | 122 | remote_rules = [] 123 | for remote_rule in os.environ.get("SEMGREP_RULES", "").split(" "): 124 | if remote_rule: 125 | remote_rules += ["--config", remote_rule] 126 | 127 | sys.argv = [ 128 | "scan", 129 | "--metrics", 130 | "off", 131 | "--disable-version-check", 132 | "--disable-nosem", 133 | *(["--autofix"] if autofix else []), 134 | "--config", 135 | "/app/rules/", 136 | *remote_rules, 137 | "--json", 138 | "--output", 139 | files.results_file, 140 | files.k8s_yaml_file, 141 | ] 142 | 143 | APP.logger.debug("+ sys.argv: %s", sys.argv) 144 | 145 | try: 146 | cli() # pylint: disable=no-value-for-parameter 147 | except SystemExit as err: 148 | APP.logger.info("+ Unexpected semgrep error recorded: %s", err) 149 | 150 | results = files.results 151 | APP.logger.debug("+ scan results: %s", results) 152 | 153 | if results["errors"]: 154 | APP.logger.error("ERROR: %s", results["errors"]) 155 | return send_response( 156 | False, uid, 418, "Request caused error during semgrep scan" 157 | ) 158 | 159 | if results["results"]: 160 | if autofix: 161 | patched_k8s_res = files.read_k8s_yaml() 162 | patches = jsonpatch.JsonPatch.from_diff(k8s_res, patched_k8s_res).patch 163 | else: 164 | patches = None 165 | num_findings = len(results["results"]) 166 | findings = "\n" + "\n".join( 167 | ["* " + f["check_id"] for f in results["results"]] 168 | ) 169 | APP.logger.debug("+ %s finding(s): %s", num_findings, findings) 170 | return send_response( 171 | autofix, 172 | uid, 173 | 202 if autofix else 403, 174 | f"Found {num_findings} violation(s) of the following policies: {findings}", 175 | patch=patches, 176 | ) 177 | except Exception as err: # pylint: disable=W0718 178 | return send_response(False, uid, 500, f"Unexpected Webhook exception: {err}") 179 | finally: 180 | files.remove_files() 181 | 182 | return send_response(True, uid, 201, "Compliant resource admitted") 183 | 184 | 185 | def send_response(allowed, uid, code, msg, patch=None): 186 | """ 187 | Prepare json response in expected format based on validation result. 188 | """ 189 | APP.logger.warning( 190 | "> response:(allowed=%s, uid=%s, status_code=%s, msg=%s, audit=%s)", 191 | allowed | AUDIT, 192 | uid, 193 | code, 194 | msg.replace("\n", ""), 195 | AUDIT, 196 | ) 197 | 198 | review = { 199 | "apiVersion": "admission.k8s.io/v1", 200 | "kind": "AdmissionReview", 201 | "response": { 202 | "allowed": allowed | AUDIT, 203 | "uid": uid, 204 | "status": {"code": code, "message": msg}, 205 | }, 206 | } 207 | 208 | if AUDIT and not allowed: 209 | review["response"]["warnings"] = ["[Semgr8s] " + msg.replace("\n", "")] 210 | 211 | if patch: 212 | review["response"]["patchType"] = "JSONPatch" 213 | review["response"]["patch"] = base64.b64encode( 214 | bytearray(json.dumps(patch), "utf-8") 215 | ).decode("utf-8") 216 | 217 | return jsonify(review) 218 | -------------------------------------------------------------------------------- /semgr8s/files.py: -------------------------------------------------------------------------------- 1 | """ 2 | File handler for all k8s yaml, results files. 3 | """ 4 | 5 | import json 6 | import os 7 | import random 8 | import string 9 | 10 | import yaml 11 | from werkzeug.utils import secure_filename 12 | 13 | DATA_FOLDER = "/app/data" 14 | RANDOM = random.SystemRandom() 15 | 16 | 17 | class S8sFiles: 18 | """ 19 | Files class for handling file management for semgr8s. 20 | """ 21 | 22 | postfix: str 23 | 24 | def __init__(self) -> None: 25 | self.postfix = self.get_random_string(length=20) 26 | 27 | @property 28 | def k8s_yaml_file(self) -> str: 29 | "Full file path of the the kubernetes yaml file." 30 | return self.generate_filename(prefix="k8s", filetype="yml") 31 | 32 | @property 33 | def results_file(self) -> str: 34 | "Full path semgrep results file." 35 | return self.generate_filename(prefix="results", filetype="json") 36 | 37 | @property 38 | def results(self) -> dict: 39 | "Get semgrep results from results file." 40 | return self.read_results() 41 | 42 | @staticmethod 43 | def get_random_string(length: int = 1) -> str: 44 | "Get random string of ascii letters and digits of length 'k'." 45 | return "".join(RANDOM.choices(string.ascii_letters + string.digits, k=length)) 46 | 47 | @staticmethod 48 | def read_json(filename: str) -> dict: 49 | "Get data from json file." 50 | with open(filename, "r", encoding="utf-8") as file: 51 | data = json.load(fp=file) 52 | return data 53 | 54 | @staticmethod 55 | def read_yaml(filename: str) -> dict: 56 | "Get data from yaml file." 57 | with open(filename, "r", encoding="utf-8") as file: 58 | data = yaml.safe_load(stream=file) 59 | return data 60 | 61 | @staticmethod 62 | def write_yaml(filename: str, data: dict) -> None: 63 | "Write data to yaml file." 64 | with open(filename, "w", encoding="utf-8") as file: 65 | yaml.safe_dump(data=data, stream=file, default_flow_style=False) 66 | 67 | def generate_filename(self, prefix: str = "", filetype: str = "") -> str: 68 | "Generate filename from prefix, postfix, and file type." 69 | return os.path.join( 70 | DATA_FOLDER, 71 | secure_filename(f"{prefix}_{self.postfix}.{filetype}"), 72 | ) 73 | 74 | def read_k8s_yaml(self) -> dict: 75 | "Get k8s resource data from k8s_yaml_file." 76 | return self.read_yaml(self.k8s_yaml_file) 77 | 78 | def write_k8s_yaml(self, data: dict) -> None: 79 | "Write yaml data to k8s_yaml_file." 80 | self.write_yaml(self.k8s_yaml_file, data=data) 81 | 82 | def read_results(self) -> dict: 83 | "Get results data from results_file." 84 | return self.read_json(self.results_file) 85 | 86 | def remove_files(self) -> None: 87 | "Remove temporary files files." 88 | try: 89 | os.remove(self.k8s_yaml_file) 90 | os.remove(self.results_file) 91 | except (FileNotFoundError, UnboundLocalError): 92 | pass 93 | -------------------------------------------------------------------------------- /semgr8s/k8s_api.py: -------------------------------------------------------------------------------- 1 | """ 2 | k8s API wrapper to request a provided path within the current environment 3 | """ 4 | 5 | import os 6 | from json.decoder import JSONDecodeError 7 | import requests 8 | 9 | from semgr8s.app import APP 10 | 11 | 12 | def request_kube_api(path: str): 13 | """ 14 | Make an API call to the underlying Kubernetes API server with the given 15 | `path`. 16 | """ 17 | 18 | token_path = "/var/run/secrets/kubernetes.io/serviceaccount/token" 19 | ca_path = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" 20 | kube_ip = os.environ.get("KUBERNETES_SERVICE_HOST") 21 | kube_port = os.environ.get("KUBERNETES_SERVICE_PORT") 22 | 23 | token = __get_token(token_path) 24 | 25 | url = f"https://{kube_ip}:{kube_port}/{path}" 26 | headers = {"Authorization": f"Bearer {token}"} 27 | 28 | try: 29 | response = requests.get(url, verify=ca_path, headers=headers, timeout=30) 30 | except JSONDecodeError as err: 31 | APP.logger.error("Malformed k8s API response or resource yaml: %s", err) 32 | return {} 33 | 34 | response.raise_for_status() 35 | 36 | return response.json() 37 | 38 | 39 | def __get_token(path: str): 40 | """ 41 | Get the API token from the container's file system. 42 | """ 43 | with open(path, "r", encoding="utf-8") as file: 44 | return file.read() 45 | -------------------------------------------------------------------------------- /semgr8s/updater.py: -------------------------------------------------------------------------------- 1 | """ 2 | Update cached rules from configmaps. 3 | """ 4 | 5 | import os 6 | 7 | from urllib.parse import urlencode 8 | 9 | from semgr8s.k8s_api import request_kube_api 10 | from semgr8s.app import APP 11 | 12 | RULESPATH = "/app/rules" 13 | 14 | 15 | def update_rules(): 16 | """ 17 | Request all rule configmaps from kubernetes api and store locally in semgrep format. 18 | """ 19 | APP.logger.debug("Updating rule set") 20 | 21 | try: 22 | old_rule_files = [ 23 | file 24 | for file in os.listdir(RULESPATH) 25 | if os.path.isfile(os.path.join(RULESPATH, file)) 26 | ] 27 | namespace = os.getenv("NAMESPACE", "default") 28 | query = {"labelSelector": "semgr8s/rule"} 29 | 30 | rules = request_kube_api( 31 | f"api/v1/namespaces/{namespace}/configmaps?{urlencode(query)}" 32 | ) 33 | 34 | for item in rules.get("items", []): 35 | data = list(item.get("data", {}).items()) 36 | for datum in data: 37 | file, content = datum 38 | path = os.path.join(RULESPATH, file) 39 | with open(path, "w", encoding="utf-8") as rule_file: 40 | rule_file.write(content) 41 | APP.logger.debug("Updated %s rule", file) 42 | try: 43 | old_rule_files.remove(file) 44 | except ValueError: 45 | pass 46 | for deprecated_rule in old_rule_files: 47 | os.remove(os.path.join(RULESPATH, deprecated_rule)) 48 | APP.logger.info("Deleted %s rule", deprecated_rule) 49 | except Exception as err: # pylint: disable=W0718 50 | APP.logger.error("Updating rules failed unexpectedly: %s", err) 51 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Run unit tests 2 | 3 | Unit tests must be run inside the semgr8s test docker image to ensure compatible file system. 4 | The test image is build as target `tester`: 5 | 6 | ```bash 7 | docker buildx build --target tester -t semgr8s:tester -f build/Dockerfile . 8 | ``` 9 | 10 | Run default unit tests: 11 | 12 | ```bash 13 | docker run --rm -it -v ${PWD}/tests/:/app/tests/ semgr8s:tester 14 | ``` 15 | 16 | Or manually via: 17 | 18 | ```bash 19 | docker run --rm -it -v ${PWD}/tests/:/app/tests/ semgr8s:tester pytest --cov=semgr8s tests/ 20 | ``` 21 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semgr8ns/semgr8s/55d4e8d831e7e2bb3ef340f22f8b5172f88443e2/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pytest 3 | import re 4 | import requests 5 | from contextlib import contextmanager 6 | 7 | 8 | import semgr8s.k8s_api 9 | 10 | """ 11 | This file is used for sharing fixtures across all other test files. 12 | https://docs.pytest.org/en/stable/fixture.html#scope-sharing-fixtures-across-classes-modules-packages-or-session 13 | """ 14 | 15 | 16 | @contextmanager 17 | def no_exc(): 18 | yield 19 | 20 | 21 | def get_json(path): 22 | with open(path, "r") as file: 23 | return json.load(file) 24 | 25 | 26 | def get_admreq(adm_type): 27 | try: 28 | return get_json( 29 | f"tests/data/sample_admission_requests/admission_request_{adm_type}.json" 30 | ) 31 | except FileNotFoundError: 32 | return None 33 | 34 | 35 | @pytest.fixture 36 | def adm_req_samples(): 37 | return [ 38 | get_admreq(t) 39 | for t in ( 40 | "empty", 41 | "no_request", 42 | "no_request_uid", 43 | "deployments", 44 | "deployments_forbiddenlabel", 45 | "pods", 46 | "pods_forbiddenlabel", 47 | ) 48 | ] 49 | 50 | 51 | def get_k8s_res(path, namespace="default"): 52 | if namespace == "default": 53 | return get_json(f"tests/data/sample_k8s_resources/{path}.json") 54 | else: 55 | return get_json(f"tests/data/sample_k8s_resources/{path}_{namespace}.json") 56 | 57 | 58 | @pytest.fixture 59 | def m_request(monkeypatch): 60 | monkeypatch.setattr(requests, "get", mock_get_request) 61 | monkeypatch.setattr(semgr8s.k8s_api, "__get_token", kube_token) 62 | 63 | 64 | class MockResponse: 65 | content: dict 66 | headers: dict 67 | status_code: int = 200 68 | 69 | def __init__(self, content: dict, headers: dict = None, status_code: int = 200): 70 | self.content = content 71 | self.headers = headers 72 | self.status_code = status_code 73 | 74 | def raise_for_status(self): 75 | if self.status_code != 200: 76 | raise requests.exceptions.HTTPError 77 | 78 | def json(self): 79 | return self.content 80 | 81 | 82 | def mock_get_request(url, **kwargs): 83 | kube_regex = [ 84 | ( 85 | r"https:\/\/[^\/]+\/apis?\/(apps\/v1|v1|batch\/v1beta1)" 86 | r"\/namespaces\/([^\/]+)\/([^\/\?]+)(\/|\?)([^\/]+)" 87 | ), 88 | mock_request_kube, 89 | ] 90 | kube_namespace_less_regex = [ 91 | ( 92 | r"https:\/\/[^\/]+\/apis?\/(admissionregistration" 93 | r"\.k8s\.io\/v1beta1)\/[^\/]+\/([^\/]+)" 94 | ), 95 | mock_request_kube_namespace_less, 96 | ] 97 | 98 | for reg in ( 99 | kube_regex, 100 | kube_namespace_less_regex, 101 | ): 102 | match = re.search(reg[0], url) 103 | 104 | if match: 105 | return reg[1](match, **kwargs) 106 | return MockResponse({}, status_code=500) 107 | 108 | 109 | def mock_request_kube(match: re.Match, **kwargs): 110 | version, namespace, kind, name = ( 111 | match.group(1), 112 | match.group(2), 113 | match.group(3), 114 | match.group(4), 115 | ) 116 | 117 | try: 118 | return MockResponse(get_k8s_res(kind, namespace)) 119 | except FileNotFoundError as err: 120 | return MockResponse({}, status_code=500) 121 | 122 | 123 | def kube_token(path: str): 124 | return "" 125 | 126 | 127 | def mock_request_kube_namespace_less(match: re.Match, **kwargs): 128 | name = match.group(2) 129 | return MockResponse(get_k8s_res(name)) 130 | -------------------------------------------------------------------------------- /tests/data/sample_admission_requests/admission_request_deployments.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "AdmissionReview", 3 | "apiVersion": "admission.k8s.io/v1beta1", 4 | "request": { 5 | "uid": "3a3a7b38-5512-4a85-94bb-3562269e0a6a", 6 | "kind": { 7 | "group": "apps", 8 | "version": "v1", 9 | "kind": "Deployment" 10 | }, 11 | "resource": { 12 | "group": "apps", 13 | "version": "v1", 14 | "resource": "deployments" 15 | }, 16 | "requestKind": { 17 | "group": "apps", 18 | "version": "v1", 19 | "kind": "Deployment" 20 | }, 21 | "requestResource": { 22 | "group": "apps", 23 | "version": "v1", 24 | "resource": "deployments" 25 | }, 26 | "namespace": "test-semgr8s", 27 | "operation": "CREATE", 28 | "userInfo": { 29 | "username": "admin", 30 | "uid": "10000", 31 | "groups": [ 32 | "system:masters", 33 | "system:authenticated" 34 | ] 35 | }, 36 | "object": { 37 | "kind": "Deployment", 38 | "apiVersion": "apps/v1", 39 | "metadata": { 40 | "name": "charlie-deployment", 41 | "namespace": "test-semgr8s", 42 | "creationTimestamp": "", 43 | "labels": { 44 | "app": "test-semgr8s", 45 | "foo": "bar" 46 | }, 47 | "annotations": {} 48 | }, 49 | "spec": { 50 | "replicas": 1, 51 | "selector": { 52 | "matchLabels": { 53 | "app": "test-semgr8s", 54 | "foo": "bar" 55 | } 56 | }, 57 | "template": { 58 | "metadata": { 59 | "creationTimestamp": "", 60 | "labels": { 61 | "app": "test-semgr8s", 62 | "foo": "bar" 63 | } 64 | }, 65 | "spec": { 66 | "containers": [ 67 | { 68 | "name": "test-semgr8s", 69 | "image": "wonderland/alice:young", 70 | "ports": [ 71 | { 72 | "containerPort": 5000, 73 | "protocol": "TCP" 74 | } 75 | ], 76 | "resources": {}, 77 | "terminationMessagePath": "/dev/termination-log", 78 | "terminationMessagePolicy": "File", 79 | "imagePullPolicy": "Always" 80 | } 81 | ], 82 | "restartPolicy": "Always", 83 | "terminationGracePeriodSeconds": 30, 84 | "dnsPolicy": "ClusterFirst", 85 | "securityContext": {}, 86 | "schedulerName": "default-scheduler" 87 | } 88 | }, 89 | "strategy": { 90 | "type": "RollingUpdate", 91 | "rollingUpdate": { 92 | "maxUnavailable": "25%", 93 | "maxSurge": "25%" 94 | } 95 | }, 96 | "revisionHistoryLimit": 10, 97 | "progressDeadlineSeconds": 600 98 | }, 99 | "status": {} 100 | }, 101 | "oldObject": {}, 102 | "dryRun": false, 103 | "options": { 104 | "kind": "CreateOptions", 105 | "apiVersion": "meta.k8s.io/v1" 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /tests/data/sample_admission_requests/admission_request_deployments_forbiddenlabel.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "AdmissionReview", 3 | "apiVersion": "admission.k8s.io/v1beta1", 4 | "request": { 5 | "uid": "3a3a7b38-5512-4a85-94bb-3562269e0a6a", 6 | "kind": { 7 | "group": "apps", 8 | "version": "v1", 9 | "kind": "Deployment" 10 | }, 11 | "resource": { 12 | "group": "apps", 13 | "version": "v1", 14 | "resource": "deployments" 15 | }, 16 | "requestKind": { 17 | "group": "apps", 18 | "version": "v1", 19 | "kind": "Deployment" 20 | }, 21 | "requestResource": { 22 | "group": "apps", 23 | "version": "v1", 24 | "resource": "deployments" 25 | }, 26 | "namespace": "test-semgr8s", 27 | "operation": "CREATE", 28 | "userInfo": { 29 | "username": "admin", 30 | "uid": "10000", 31 | "groups": [ 32 | "system:masters", 33 | "system:authenticated" 34 | ] 35 | }, 36 | "object": { 37 | "kind": "Deployment", 38 | "apiVersion": "apps/v1", 39 | "metadata": { 40 | "name": "charlie-deployment", 41 | "namespace": "test-semgr8s", 42 | "creationTimestamp": "", 43 | "labels": { 44 | "app": "test-semgr8s", 45 | "semgr8s-test": "forbidden-test-label-e3b0c44298fc1c", 46 | "foo": "bar" 47 | }, 48 | "annotations": {} 49 | }, 50 | "spec": { 51 | "replicas": 1, 52 | "selector": { 53 | "matchLabels": { 54 | "app": "test-semgr8s", 55 | "semgr8s-test": "forbidden-test-label-e3b0c44298fc1c", 56 | "foo": "bar" 57 | } 58 | }, 59 | "template": { 60 | "metadata": { 61 | "creationTimestamp": "", 62 | "labels": { 63 | "app": "test-semgr8s", 64 | "semgr8s-test": "forbidden-test-label-e3b0c44298fc1c", 65 | "foo": "bar" 66 | } 67 | }, 68 | "spec": { 69 | "containers": [ 70 | { 71 | "name": "test-semgr8s", 72 | "image": "wonderland/alice:young", 73 | "ports": [ 74 | { 75 | "containerPort": 5000, 76 | "protocol": "TCP" 77 | } 78 | ], 79 | "resources": {}, 80 | "terminationMessagePath": "/dev/termination-log", 81 | "terminationMessagePolicy": "File", 82 | "imagePullPolicy": "Always" 83 | } 84 | ], 85 | "restartPolicy": "Always", 86 | "terminationGracePeriodSeconds": 30, 87 | "dnsPolicy": "ClusterFirst", 88 | "securityContext": {}, 89 | "schedulerName": "default-scheduler" 90 | } 91 | }, 92 | "strategy": { 93 | "type": "RollingUpdate", 94 | "rollingUpdate": { 95 | "maxUnavailable": "25%", 96 | "maxSurge": "25%" 97 | } 98 | }, 99 | "revisionHistoryLimit": 10, 100 | "progressDeadlineSeconds": 600 101 | }, 102 | "status": {} 103 | }, 104 | "oldObject": {}, 105 | "dryRun": false, 106 | "options": { 107 | "kind": "CreateOptions", 108 | "apiVersion": "meta.k8s.io/v1" 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /tests/data/sample_admission_requests/admission_request_empty.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /tests/data/sample_admission_requests/admission_request_no_request.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "AdmissionReview", 3 | "apiVersion": "admission.k8s.io/v1beta1", 4 | "request": {}, 5 | "dryRun": false 6 | } 7 | -------------------------------------------------------------------------------- /tests/data/sample_admission_requests/admission_request_no_request_uid.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "AdmissionReview", 3 | "apiVersion": "admission.k8s.io/v1beta1", 4 | "request": { 5 | "kind": { 6 | "group": "", 7 | "version": "v1", 8 | "kind": "Pod" 9 | }, 10 | "resource": { 11 | "group": "", 12 | "version": "v1", 13 | "resource": "pods" 14 | }, 15 | "namespace": "test-semgr8s", 16 | "operation": "CREATE", 17 | "userInfo": { 18 | "username": "system:serviceaccount:kube-system:replicaset-controller", 19 | "uid": "13076e42-1513-11ea-b3fc-02897404852e", 20 | "groups": [ 21 | "system:serviceaccounts", 22 | "system:serviceaccounts:kube-system", 23 | "system:authenticated" 24 | ] 25 | }, 26 | "object": { 27 | "kind": "Pod", 28 | "apiVersion": "v1", 29 | "metadata": { 30 | "generateName": "charlie-deployment-76fbf58b7d-", 31 | "creationTimestamp": "", 32 | "labels": { 33 | "app": "test-semgr8s", 34 | "foo": "bar", 35 | "pod-template-hash": "76fbf58b7d" 36 | }, 37 | "annotations": { 38 | "kubernetes.io/psp": "eks.privileged" 39 | }, 40 | "ownerReferences": [ 41 | { 42 | "apiVersion": "apps/v1", 43 | "kind": "ReplicaSet", 44 | "name": "charlie-deployment-76fbf58b7d", 45 | "uid": "090d26f8-1812-11ea-b3fc-02897404852e", 46 | "controller": true, 47 | "blockOwnerDeletion": true 48 | } 49 | ] 50 | }, 51 | "spec": { 52 | "volumes": [ 53 | { 54 | "name": "default-token-hn7nn", 55 | "secret": { 56 | "secretName": "default-token-hn7nn" 57 | } 58 | } 59 | ], 60 | "containers": [ 61 | { 62 | "name": "test-semgr8s", 63 | "image": "fairytale/crazy:hatter", 64 | "ports": [ 65 | { 66 | "containerPort": 5000, 67 | "protocol": "TCP" 68 | } 69 | ], 70 | "resources": {}, 71 | "volumeMounts": [ 72 | { 73 | "name": "default-token-hn7nn", 74 | "readOnly": true, 75 | "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount" 76 | } 77 | ], 78 | "terminationMessagePath": "/dev/termination-log", 79 | "terminationMessagePolicy": "File", 80 | "imagePullPolicy": "Always" 81 | } 82 | ], 83 | "restartPolicy": "Always", 84 | "terminationGracePeriodSeconds": 30, 85 | "dnsPolicy": "ClusterFirst", 86 | "serviceAccountName": "default", 87 | "serviceAccount": "default", 88 | "securityContext": {}, 89 | "schedulerName": "default-scheduler", 90 | "tolerations": [ 91 | { 92 | "key": "node.kubernetes.io/not-ready", 93 | "operator": "Exists", 94 | "effect": "NoExecute", 95 | "tolerationSeconds": 300 96 | }, 97 | { 98 | "key": "node.kubernetes.io/unreachable", 99 | "operator": "Exists", 100 | "effect": "NoExecute", 101 | "tolerationSeconds": 300 102 | } 103 | ], 104 | "priority": 0, 105 | "enableServiceLinks": true 106 | }, 107 | "status": {} 108 | }, 109 | "oldObject": {} 110 | }, 111 | "dryRun": false 112 | } 113 | -------------------------------------------------------------------------------- /tests/data/sample_admission_requests/admission_request_pods.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "AdmissionReview", 3 | "apiVersion": "admission.k8s.io/v1beta1", 4 | "request": { 5 | "uid": "0c3331b6-1812-11ea-b3fc-02897404852e", 6 | "kind": { 7 | "group": "", 8 | "version": "v1", 9 | "kind": "Pod" 10 | }, 11 | "resource": { 12 | "group": "", 13 | "version": "v1", 14 | "resource": "pods" 15 | }, 16 | "namespace": "test-semgr8s", 17 | "operation": "CREATE", 18 | "userInfo": { 19 | "username": "system:serviceaccount:kube-system:replicaset-controller", 20 | "uid": "13076e42-1513-11ea-b3fc-02897404852e", 21 | "groups": [ 22 | "system:serviceaccounts", 23 | "system:serviceaccounts:kube-system", 24 | "system:authenticated" 25 | ] 26 | }, 27 | "object": { 28 | "kind": "Pod", 29 | "apiVersion": "v1", 30 | "metadata": { 31 | "generateName": "charlie-deployment-76fbf58b7d-", 32 | "creationTimestamp": "", 33 | "labels": { 34 | "app": "test-semgr8s", 35 | "foo": "bar", 36 | "pod-template-hash": "76fbf58b7d" 37 | }, 38 | "annotations": { 39 | "kubernetes.io/psp": "eks.privileged" 40 | }, 41 | "ownerReferences": [ 42 | { 43 | "apiVersion": "apps/v1", 44 | "kind": "ReplicaSet", 45 | "name": "charlie-deployment-76fbf58b7d", 46 | "uid": "090d26f8-1812-11ea-b3fc-02897404852e", 47 | "controller": true, 48 | "blockOwnerDeletion": true 49 | } 50 | ] 51 | }, 52 | "spec": { 53 | "volumes": [ 54 | { 55 | "name": "default-token-hn7nn", 56 | "secret": { 57 | "secretName": "default-token-hn7nn" 58 | } 59 | } 60 | ], 61 | "containers": [ 62 | { 63 | "name": "test-semgr8s", 64 | "image": "fairytale/crazy:hatter", 65 | "ports": [ 66 | { 67 | "containerPort": 5000, 68 | "protocol": "TCP" 69 | } 70 | ], 71 | "resources": {}, 72 | "volumeMounts": [ 73 | { 74 | "name": "default-token-hn7nn", 75 | "readOnly": true, 76 | "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount" 77 | } 78 | ], 79 | "terminationMessagePath": "/dev/termination-log", 80 | "terminationMessagePolicy": "File", 81 | "imagePullPolicy": "Always" 82 | } 83 | ], 84 | "restartPolicy": "Always", 85 | "terminationGracePeriodSeconds": 30, 86 | "dnsPolicy": "ClusterFirst", 87 | "serviceAccountName": "default", 88 | "serviceAccount": "default", 89 | "securityContext": {}, 90 | "schedulerName": "default-scheduler", 91 | "tolerations": [ 92 | { 93 | "key": "node.kubernetes.io/not-ready", 94 | "operator": "Exists", 95 | "effect": "NoExecute", 96 | "tolerationSeconds": 300 97 | }, 98 | { 99 | "key": "node.kubernetes.io/unreachable", 100 | "operator": "Exists", 101 | "effect": "NoExecute", 102 | "tolerationSeconds": 300 103 | } 104 | ], 105 | "priority": 0, 106 | "enableServiceLinks": true 107 | }, 108 | "status": {} 109 | }, 110 | "oldObject": {} 111 | }, 112 | "dryRun": false 113 | } 114 | -------------------------------------------------------------------------------- /tests/data/sample_admission_requests/admission_request_pods_forbiddenlabel.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "AdmissionReview", 3 | "apiVersion": "admission.k8s.io/v1beta1", 4 | "request": { 5 | "uid": "0c3331b6-1812-11ea-b3fc-02897404852e", 6 | "kind": { 7 | "group": "", 8 | "version": "v1", 9 | "kind": "Pod" 10 | }, 11 | "resource": { 12 | "group": "", 13 | "version": "v1", 14 | "resource": "pods" 15 | }, 16 | "namespace": "test-semgr8s", 17 | "operation": "CREATE", 18 | "userInfo": { 19 | "username": "system:serviceaccount:kube-system:replicaset-controller", 20 | "uid": "13076e42-1513-11ea-b3fc-02897404852e", 21 | "groups": [ 22 | "system:serviceaccounts", 23 | "system:serviceaccounts:kube-system", 24 | "system:authenticated" 25 | ] 26 | }, 27 | "object": { 28 | "kind": "Pod", 29 | "apiVersion": "v1", 30 | "metadata": { 31 | "generateName": "charlie-deployment-76fbf58b7d-", 32 | "creationTimestamp": "", 33 | "labels": { 34 | "app": "test-semgr8s", 35 | "semgr8s-test": "forbidden-test-label-e3b0c44298fc1c", 36 | "foo": "bar", 37 | "pod-template-hash": "76fbf58b7d" 38 | }, 39 | "annotations": { 40 | "kubernetes.io/psp": "eks.privileged" 41 | }, 42 | "ownerReferences": [ 43 | { 44 | "apiVersion": "apps/v1", 45 | "kind": "ReplicaSet", 46 | "name": "charlie-deployment-76fbf58b7d", 47 | "uid": "090d26f8-1812-11ea-b3fc-02897404852e", 48 | "controller": true, 49 | "blockOwnerDeletion": true 50 | } 51 | ] 52 | }, 53 | "spec": { 54 | "volumes": [ 55 | { 56 | "name": "default-token-hn7nn", 57 | "secret": { 58 | "secretName": "default-token-hn7nn" 59 | } 60 | } 61 | ], 62 | "containers": [ 63 | { 64 | "name": "test-semgr8s", 65 | "image": "fairytale/crazy:hatter", 66 | "ports": [ 67 | { 68 | "containerPort": 5000, 69 | "protocol": "TCP" 70 | } 71 | ], 72 | "resources": {}, 73 | "volumeMounts": [ 74 | { 75 | "name": "default-token-hn7nn", 76 | "readOnly": true, 77 | "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount" 78 | } 79 | ], 80 | "terminationMessagePath": "/dev/termination-log", 81 | "terminationMessagePolicy": "File", 82 | "imagePullPolicy": "Always" 83 | } 84 | ], 85 | "restartPolicy": "Always", 86 | "terminationGracePeriodSeconds": 30, 87 | "dnsPolicy": "ClusterFirst", 88 | "serviceAccountName": "default", 89 | "serviceAccount": "default", 90 | "securityContext": {}, 91 | "schedulerName": "default-scheduler", 92 | "tolerations": [ 93 | { 94 | "key": "node.kubernetes.io/not-ready", 95 | "operator": "Exists", 96 | "effect": "NoExecute", 97 | "tolerationSeconds": 300 98 | }, 99 | { 100 | "key": "node.kubernetes.io/unreachable", 101 | "operator": "Exists", 102 | "effect": "NoExecute", 103 | "tolerationSeconds": 300 104 | } 105 | ], 106 | "priority": 0, 107 | "enableServiceLinks": true 108 | }, 109 | "status": {} 110 | }, 111 | "oldObject": {} 112 | }, 113 | "dryRun": false 114 | } 115 | -------------------------------------------------------------------------------- /tests/data/sample_k8s_resources/configmaps.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "v1", 3 | "items": [ 4 | { 5 | "apiVersion": "v1", 6 | "data": { 7 | "test-semgr8s-forbidden-label.yaml": "rules:\n- id: test-semgr8s-forbidden-label\n patterns:\n - pattern-inside: |\n metadata:\n ...\n - pattern-inside: |\n labels:\n ...\n - pattern: |\n semgr8s-test: forbidden-test-label-e3b0c44298fc1c\n fix: \"\"\n message: TEST ONLY. Found kubernetes resource with semgr8s forbidden test label. Any resource with label \"semgr8s-test=forbidden-test-label-e3b0c44298fc1c\" is denied. This label carries no meaning beyond testing and demonstration purposes.\n metadata:\n category: test \n technology:\n - kubernetes\n references:\n - https://semgr8ns.github.io/semgr8s/latest/#testing\n languages: [yaml]\n severity: INFO\n" 8 | }, 9 | "kind": "ConfigMap", 10 | "metadata": { 11 | "annotations": { 12 | "meta.helm.sh/release-name": "semgr8s", 13 | "meta.helm.sh/release-namespace": "semgr8ns" 14 | }, 15 | "creationTimestamp": "2024-02-29T16:55:00Z", 16 | "labels": { 17 | "app.kubernetes.io/instance": "semgr8s", 18 | "app.kubernetes.io/managed-by": "Helm", 19 | "app.kubernetes.io/name": "semgr8s", 20 | "app.kubernetes.io/version": "0.1.5", 21 | "helm.sh/chart": "semgr8s-0.1.5", 22 | "semgr8s/rule": "true" 23 | }, 24 | "name": "test-semgr8s-forbidden-label", 25 | "namespace": "semgr8ns", 26 | "resourceVersion": "1088322", 27 | "uid": "1ccd87f6-fe42-4428-910a-978643a44409" 28 | } 29 | }, 30 | { 31 | "apiVersion": "v1", 32 | "data": { 33 | "tester-test-name.yaml": "rules:\n- id: test-semgr8s-forbidden-label\n patterns:\n - pattern-inside: |\n metadata:\n ...\n - pattern-inside: |\n labels:\n ...\n - pattern: |\n semgr8s-test: forbidden-test-label-e3b0c44298fc1c\n fix: \"\"\n message: TEST ONLY. Found kubernetes resource with semgr8s forbidden test label. Any resource with label \"semgr8s-test=forbidden-test-label-e3b0c44298fc1c\" is denied. This label carries no meaning beyond testing and demonstration purposes.\n metadata:\n category: test \n technology:\n - kubernetes\n references:\n - https://semgr8ns.github.io/semgr8s/latest/#testing\n languages: [yaml]\n severity: INFO\n" 34 | }, 35 | "kind": "ConfigMap", 36 | "metadata": { 37 | "annotations": { 38 | "meta.helm.sh/release-name": "semgr8s", 39 | "meta.helm.sh/release-namespace": "semgr8ns" 40 | }, 41 | "creationTimestamp": "2024-02-29T16:55:00Z", 42 | "labels": { 43 | "app.kubernetes.io/instance": "semgr8s", 44 | "app.kubernetes.io/managed-by": "Helm", 45 | "app.kubernetes.io/name": "semgr8s", 46 | "app.kubernetes.io/version": "0.1.5", 47 | "helm.sh/chart": "semgr8s-0.1.5", 48 | "semgr8s/rule": "true" 49 | }, 50 | "name": "tester-test-name", 51 | "namespace": "semgr8ns", 52 | "resourceVersion": "1088323", 53 | "uid": "9404b1fc-7f93-41f2-a006-4326c327e356" 54 | } 55 | } 56 | ], 57 | "kind": "List", 58 | "metadata": { 59 | "resourceVersion": "" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/data/sample_k8s_resources/configmaps_broken_nodata.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "v1", 3 | "items": [ 4 | { 5 | "apiVersion": "v1", 6 | "data": {}, 7 | "kind": "ConfigMap", 8 | "metadata": { 9 | "annotations": { 10 | "meta.helm.sh/release-name": "semgr8s", 11 | "meta.helm.sh/release-namespace": "semgr8ns" 12 | }, 13 | "creationTimestamp": "2024-02-29T16:55:00Z", 14 | "labels": { 15 | "app.kubernetes.io/instance": "semgr8s", 16 | "app.kubernetes.io/managed-by": "Helm", 17 | "app.kubernetes.io/name": "semgr8s", 18 | "app.kubernetes.io/version": "0.1.5", 19 | "helm.sh/chart": "semgr8s-0.1.5", 20 | "semgr8s/rule": "true" 21 | }, 22 | "name": "test-semgr8s-forbidden-label", 23 | "namespace": "semgr8ns", 24 | "resourceVersion": "1088322", 25 | "uid": "1ccd87f6-fe42-4428-910a-978643a44409" 26 | } 27 | }, 28 | { 29 | "apiVersion": "v1", 30 | "data": { 31 | "tester-test-broken.yaml": "rules:\n- id: test-semgr8s-forbidden-label\n patterns:\n - pattern-inside: |\n metadata:\n ...\n - pattern-inside: |\n labels:\n ...\n - pattern: |\n semgr8s-test: forbidden-test-label-e3b0c44298fc1c\n fix: \"\"\n message: TEST ONLY. Found kubernetes resource with semgr8s forbidden test label. Any resource with label \"semgr8s-test=forbidden-test-label-e3b0c44298fc1c\" is denied. This label carries no meaning beyond testing and demonstration purposes.\n metadata:\n category: test \n technology:\n - kubernetes\n references:\n - https://semgr8ns.github.io/semgr8s/latest/#testing\n languages: [yaml]\n severity: INFO\n" 32 | }, 33 | "kind": "ConfigMap", 34 | "metadata": { 35 | "annotations": { 36 | "meta.helm.sh/release-name": "semgr8s", 37 | "meta.helm.sh/release-namespace": "semgr8ns" 38 | }, 39 | "creationTimestamp": "2024-02-29T16:55:00Z", 40 | "labels": { 41 | "app.kubernetes.io/instance": "semgr8s", 42 | "app.kubernetes.io/managed-by": "Helm", 43 | "app.kubernetes.io/name": "semgr8s", 44 | "app.kubernetes.io/version": "0.1.5", 45 | "helm.sh/chart": "semgr8s-0.1.5", 46 | "semgr8s/rule": "true" 47 | }, 48 | "name": "tester-test-name", 49 | "namespace": "semgr8ns", 50 | "resourceVersion": "1088323", 51 | "uid": "9404b1fc-7f93-41f2-a006-4326c327e356" 52 | } 53 | } 54 | ], 55 | "kind": "List", 56 | "metadata": { 57 | "resourceVersion": "" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/data/sample_k8s_resources/configmaps_broken_nojson.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "v1", 3 | "items": [ 4 | { 5 | "apiVersion": "v1", 6 | "data": { 7 | "sdf" 8 | }, 9 | "kind": "ConfigMap", 10 | "metadata": { 11 | "annotations": { 12 | "meta.helm.sh/release-name": "semgr8s", 13 | "meta.helm.sh/release-namespace": "semgr8ns" 14 | }, 15 | "creationTimestamp": "2024-02-29T16:55:00Z", 16 | "labels": { 17 | "app.kubernetes.io/instance": "semgr8s", 18 | "app.kubernetes.io/managed-by": "Helm", 19 | "app.kubernetes.io/name": "semgr8s", 20 | "app.kubernetes.io/version": "0.1.5", 21 | "helm.sh/chart": "semgr8s-0.1.5", 22 | "semgr8s/rule": "true" 23 | }, 24 | "name": "test-semgr8s-forbidden-label", 25 | "namespace": "semgr8ns", 26 | "resourceVersion": "1088322", 27 | "uid": "1ccd87f6-fe42-4428-910a-978643a44409" 28 | } 29 | }, 30 | { 31 | "apiVersion": "v1", 32 | "data": { 33 | "tester-test-broken_nojson.yaml": "rules:\n- id: test-semgr8s-forbidden-label\n patterns:\n - pattern-inside: |\n metadata:\n ...\n - pattern-inside: |\n labels:\n ...\n - pattern: |\n semgr8s-test: forbidden-test-label-e3b0c44298fc1c\n fix: \"\"\n message: TEST ONLY. Found kubernetes resource with semgr8s forbidden test label. Any resource with label \"semgr8s-test=forbidden-test-label-e3b0c44298fc1c\" is denied. This label carries no meaning beyond testing and demonstration purposes.\n metadata:\n category: test \n technology:\n - kubernetes\n references:\n - https://semgr8ns.github.io/semgr8s/latest/#testing\n languages: [yaml]\n severity: INFO\n" 34 | }, 35 | "kind": "ConfigMap", 36 | "metadata": { 37 | "annotations": { 38 | "meta.helm.sh/release-name": "semgr8s", 39 | "meta.helm.sh/release-namespace": "semgr8ns" 40 | }, 41 | "creationTimestamp": "2024-02-29T16:55:00Z", 42 | "labels": { 43 | "app.kubernetes.io/instance": "semgr8s", 44 | "app.kubernetes.io/managed-by": "Helm", 45 | "app.kubernetes.io/name": "semgr8s", 46 | "app.kubernetes.io/version": "0.1.5", 47 | "helm.sh/chart": "semgr8s-0.1.5", 48 | "semgr8s/rule": "true" 49 | }, 50 | "name": "tester-test-name", 51 | "namespace": "semgr8ns", 52 | "resourceVersion": "1088323", 53 | "uid": "9404b1fc-7f93-41f2-a006-4326c327e356" 54 | } 55 | } 56 | ], 57 | "kind": "List", 58 | "metadata": { 59 | "resourceVersion": "" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/data/sample_k8s_resources/configmaps_multiplerulesinmap.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "v1", 3 | "items": [ 4 | { 5 | "apiVersion": "v1", 6 | "data": { 7 | "test-semgr8s-forbidden-label.yaml": "rules:\n- id: test-semgr8s-forbidden-label\n patterns:\n - pattern-inside: |\n metadata:\n ...\n - pattern-inside: |\n labels:\n ...\n - pattern: |\n semgr8s-test: forbidden-test-label-e3b0c44298fc1c\n fix: \"\"\n message: TEST ONLY. Found kubernetes resource with semgr8s forbidden test label. Any resource with label \"semgr8s-test=forbidden-test-label-e3b0c44298fc1c\" is denied. This label carries no meaning beyond testing and demonstration purposes.\n metadata:\n category: test \n technology:\n - kubernetes\n references:\n - https://semgr8ns.github.io/semgr8s/latest/#testing\n languages: [yaml]\n severity: INFO\n" 8 | }, 9 | "kind": "ConfigMap", 10 | "metadata": { 11 | "annotations": { 12 | "meta.helm.sh/release-name": "semgr8s", 13 | "meta.helm.sh/release-namespace": "semgr8ns" 14 | }, 15 | "creationTimestamp": "2024-02-29T16:55:00Z", 16 | "labels": { 17 | "app.kubernetes.io/instance": "semgr8s", 18 | "app.kubernetes.io/managed-by": "Helm", 19 | "app.kubernetes.io/name": "semgr8s", 20 | "app.kubernetes.io/version": "0.1.5", 21 | "helm.sh/chart": "semgr8s-0.1.5", 22 | "semgr8s/rule": "true" 23 | }, 24 | "name": "test-semgr8s-forbidden-label", 25 | "namespace": "semgr8ns", 26 | "resourceVersion": "1088322", 27 | "uid": "1ccd87f6-fe42-4428-910a-978643a44409" 28 | } 29 | }, 30 | { 31 | "apiVersion": "v1", 32 | "data": { 33 | "tester-test-multiplerules1.yaml": "rules:\n- id: test-semgr8s-forbidden-label\n patterns:\n - pattern-inside: |\n metadata:\n ...\n - pattern-inside: |\n labels:\n ...\n - pattern: |\n semgr8s-test: forbidden-test-label-e3b0c44298fc1c\n fix: \"\"\n message: TEST ONLY. Found kubernetes resource with semgr8s forbidden test label. Any resource with label \"semgr8s-test=forbidden-test-label-e3b0c44298fc1c\" is denied. This label carries no meaning beyond testing and demonstration purposes.\n metadata:\n category: test \n technology:\n - kubernetes\n references:\n - https://semgr8ns.github.io/semgr8s/latest/#testing\n languages: [yaml]\n severity: INFO\n", 34 | "tester-test-multiplerules2.yaml": "rules:\n- id: test-semgr8s-forbidden-label\n patterns:\n - pattern-inside: |\n metadata:\n ...\n - pattern-inside: |\n labels:\n ...\n - pattern: |\n semgr8s-test: forbidden-test-label-e3b0c44298fc1c\n fix: \"\"\n message: TEST ONLY. Found kubernetes resource with semgr8s forbidden test label. Any resource with label \"semgr8s-test=forbidden-test-label-e3b0c44298fc1c\" is denied. This label carries no meaning beyond testing and demonstration purposes.\n metadata:\n category: test \n technology:\n - kubernetes\n references:\n - https://semgr8ns.github.io/semgr8s/latest/#testing\n languages: [yaml]\n severity: INFO\n" 35 | }, 36 | "kind": "ConfigMap", 37 | "metadata": { 38 | "annotations": { 39 | "meta.helm.sh/release-name": "semgr8s", 40 | "meta.helm.sh/release-namespace": "semgr8ns" 41 | }, 42 | "creationTimestamp": "2024-02-29T16:55:00Z", 43 | "labels": { 44 | "app.kubernetes.io/instance": "semgr8s", 45 | "app.kubernetes.io/managed-by": "Helm", 46 | "app.kubernetes.io/name": "semgr8s", 47 | "app.kubernetes.io/version": "0.1.5", 48 | "helm.sh/chart": "semgr8s-0.1.5", 49 | "semgr8s/rule": "true" 50 | }, 51 | "name": "tester-test-name", 52 | "namespace": "semgr8ns", 53 | "resourceVersion": "1088323", 54 | "uid": "9404b1fc-7f93-41f2-a006-4326c327e356" 55 | } 56 | } 57 | ], 58 | "kind": "List", 59 | "metadata": { 60 | "resourceVersion": "" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/data/sample_k8s_resources/configmaps_semgr8ns.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "v1", 3 | "items": [ 4 | { 5 | "apiVersion": "v1", 6 | "data": { 7 | "test-semgr8s-forbidden-label.yaml": "rules:\n- id: test-semgr8s-forbidden-label\n patterns:\n - pattern-inside: |\n metadata:\n ...\n - pattern-inside: |\n labels:\n ...\n - pattern: |\n semgr8s-test: forbidden-test-label-e3b0c44298fc1c\n fix: \"\"\n message: TEST ONLY. Found kubernetes resource with semgr8s forbidden test label. Any resource with label \"semgr8s-test=forbidden-test-label-e3b0c44298fc1c\" is denied. This label carries no meaning beyond testing and demonstration purposes.\n metadata:\n category: test \n technology:\n - kubernetes\n references:\n - https://semgr8ns.github.io/semgr8s/latest/#testing\n languages: [yaml]\n severity: INFO\n" 8 | }, 9 | "kind": "ConfigMap", 10 | "metadata": { 11 | "annotations": { 12 | "meta.helm.sh/release-name": "semgr8s", 13 | "meta.helm.sh/release-namespace": "semgr8ns" 14 | }, 15 | "creationTimestamp": "2024-02-29T16:55:00Z", 16 | "labels": { 17 | "app.kubernetes.io/instance": "semgr8s", 18 | "app.kubernetes.io/managed-by": "Helm", 19 | "app.kubernetes.io/name": "semgr8s", 20 | "app.kubernetes.io/version": "0.1.5", 21 | "helm.sh/chart": "semgr8s-0.1.5", 22 | "semgr8s/rule": "true" 23 | }, 24 | "name": "test-semgr8s-forbidden-label", 25 | "namespace": "semgr8ns", 26 | "resourceVersion": "1088322", 27 | "uid": "1ccd87f6-fe42-4428-910a-978643a44409" 28 | } 29 | }, 30 | { 31 | "apiVersion": "v1", 32 | "data": { 33 | "tester-test-name1.yaml": "rules:\n- id: test-semgr8s-forbidden-label\n patterns:\n - pattern-inside: |\n metadata:\n ...\n - pattern-inside: |\n labels:\n ...\n - pattern: |\n semgr8s-test: forbidden-test-label-e3b0c44298fc1c\n fix: \"\"\n message: TEST ONLY. Found kubernetes resource with semgr8s forbidden test label. Any resource with label \"semgr8s-test=forbidden-test-label-e3b0c44298fc1c\" is denied. This label carries no meaning beyond testing and demonstration purposes.\n metadata:\n category: test \n technology:\n - kubernetes\n references:\n - https://semgr8ns.github.io/semgr8s/latest/#testing\n languages: [yaml]\n severity: INFO\n" 34 | }, 35 | "kind": "ConfigMap", 36 | "metadata": { 37 | "annotations": { 38 | "meta.helm.sh/release-name": "semgr8s", 39 | "meta.helm.sh/release-namespace": "semgr8ns" 40 | }, 41 | "creationTimestamp": "2024-02-29T16:55:00Z", 42 | "labels": { 43 | "app.kubernetes.io/instance": "semgr8s", 44 | "app.kubernetes.io/managed-by": "Helm", 45 | "app.kubernetes.io/name": "semgr8s", 46 | "app.kubernetes.io/version": "0.1.5", 47 | "helm.sh/chart": "semgr8s-0.1.5", 48 | "semgr8s/rule": "true" 49 | }, 50 | "name": "tester-test-name", 51 | "namespace": "semgr8ns", 52 | "resourceVersion": "1088323", 53 | "uid": "9404b1fc-7f93-41f2-a006-4326c327e356" 54 | } 55 | } 56 | ], 57 | "kind": "List", 58 | "metadata": { 59 | "resourceVersion": "" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/data/sample_k8s_resources/deployments.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "apps/v1", 3 | "kind": "Deployment", 4 | "metadata": { 5 | "annotations": { 6 | "deployment.kubernetes.io/revision": "1", 7 | "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"test-connaisseur\"},\"name\":\"sample-san-sama-deployment\",\"namespace\":\"test-connaisseur\"},\"spec\":{\"replicas\":1,\"selector\":{\"matchLabels\":{\"app\":\"test-connaisseur\"}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"test-connaisseur\"}},\"spec\":{\"containers\":[{\"image\":\"securesystemsengineering/sample-san-sama:hai\",\"imagePullPolicy\":\"Always\",\"name\":\"test-connaisseur\",\"ports\":[{\"containerPort\":5000}]}]}}}}\n" 8 | }, 9 | "creationTimestamp": "2020-04-16T08:50:11Z", 10 | "generation": 1, 11 | "labels": { 12 | "app": "test-connaisseur" 13 | }, 14 | "name": "charlie-deployment", 15 | "namespace": "test-connaisseur", 16 | "resourceVersion": "974169", 17 | "selfLink": "/apis/apps/v1/namespaces/test-connaisseur/deployments/sample-san-sama-deployment", 18 | "uid": "3a3a7b38-5512-4a85-94bb-3562269e0a6b" 19 | }, 20 | "spec": { 21 | "progressDeadlineSeconds": 600, 22 | "replicas": 1, 23 | "revisionHistoryLimit": 10, 24 | "selector": { 25 | "matchLabels": { 26 | "app": "test-connaisseur" 27 | } 28 | }, 29 | "strategy": { 30 | "rollingUpdate": { 31 | "maxSurge": "25%", 32 | "maxUnavailable": "25%" 33 | }, 34 | "type": "RollingUpdate" 35 | }, 36 | "template": { 37 | "metadata": { 38 | "creationTimestamp": null, 39 | "labels": { 40 | "app": "test-connaisseur" 41 | } 42 | }, 43 | "spec": { 44 | "containers": [ 45 | { 46 | "image": "securesystemsengineering/sample-san-sama:hai", 47 | "imagePullPolicy": "Always", 48 | "name": "test-connaisseur", 49 | "ports": [ 50 | { 51 | "containerPort": 5000, 52 | "protocol": "TCP" 53 | } 54 | ], 55 | "resources": {}, 56 | "terminationMessagePath": "/dev/termination-log", 57 | "terminationMessagePolicy": "File" 58 | } 59 | ], 60 | "dnsPolicy": "ClusterFirst", 61 | "restartPolicy": "Always", 62 | "schedulerName": "default-scheduler", 63 | "securityContext": {}, 64 | "terminationGracePeriodSeconds": 30 65 | } 66 | } 67 | }, 68 | "status": { 69 | "availableReplicas": 1, 70 | "conditions": [ 71 | { 72 | "lastTransitionTime": "2020-04-16T08:50:15Z", 73 | "lastUpdateTime": "2020-04-16T08:50:15Z", 74 | "message": "Deployment has minimum availability.", 75 | "reason": "MinimumReplicasAvailable", 76 | "status": "True", 77 | "type": "Available" 78 | }, 79 | { 80 | "lastTransitionTime": "2020-04-16T08:50:11Z", 81 | "lastUpdateTime": "2020-04-16T08:50:15Z", 82 | "message": "ReplicaSet \"sample-san-sama-deployment-84d95bbc48\" has successfully progressed.", 83 | "reason": "NewReplicaSetAvailable", 84 | "status": "True", 85 | "type": "Progressing" 86 | } 87 | ], 88 | "observedGeneration": 1, 89 | "readyReplicas": 1, 90 | "replicas": 1, 91 | "updatedReplicas": 1 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/data/sample_k8s_resources/pods.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "v1", 3 | "kind": "Pod", 4 | "metadata": { 5 | "creationTimestamp": "2020-04-16T08:50:12Z", 6 | "generateName": "sample-san-sama-deployment-84d95bbc48-", 7 | "labels": { 8 | "app": "test-connaisseur", 9 | "pod-template-hash": "84d95bbc48" 10 | }, 11 | "name": "sample-san-sama-deployment-84d95bbc48-qqrkv", 12 | "namespace": "test-connaisseur", 13 | "ownerReferences": [ 14 | { 15 | "apiVersion": "apps/v1", 16 | "blockOwnerDeletion": true, 17 | "controller": true, 18 | "kind": "ReplicaSet", 19 | "name": "sample-san-sama-deployment-84d95bbc48", 20 | "uid": "60c2cece-3496-411b-b83e-2f0772c15fa0" 21 | } 22 | ], 23 | "resourceVersion": "974167", 24 | "selfLink": "/api/v1/namespaces/test-connaisseur/pods/sample-san-sama-deployment-84d95bbc48-qqrkv", 25 | "uid": "66ff751d-0c48-4f37-8873-4a70679d5999" 26 | }, 27 | "spec": { 28 | "containers": [ 29 | { 30 | "image": "securesystemsengineering/sample-san-sama:hai", 31 | "imagePullPolicy": "Always", 32 | "name": "test-connaisseur", 33 | "ports": [ 34 | { 35 | "containerPort": 5000, 36 | "protocol": "TCP" 37 | } 38 | ], 39 | "resources": {}, 40 | "terminationMessagePath": "/dev/termination-log", 41 | "terminationMessagePolicy": "File", 42 | "volumeMounts": [ 43 | { 44 | "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", 45 | "name": "default-token-rds7b", 46 | "readOnly": true 47 | } 48 | ] 49 | } 50 | ], 51 | "dnsPolicy": "ClusterFirst", 52 | "enableServiceLinks": true, 53 | "nodeName": "minikube", 54 | "priority": 0, 55 | "restartPolicy": "Always", 56 | "schedulerName": "default-scheduler", 57 | "securityContext": {}, 58 | "serviceAccount": "default", 59 | "serviceAccountName": "default", 60 | "terminationGracePeriodSeconds": 30, 61 | "tolerations": [ 62 | { 63 | "effect": "NoExecute", 64 | "key": "node.kubernetes.io/not-ready", 65 | "operator": "Exists", 66 | "tolerationSeconds": 300 67 | }, 68 | { 69 | "effect": "NoExecute", 70 | "key": "node.kubernetes.io/unreachable", 71 | "operator": "Exists", 72 | "tolerationSeconds": 300 73 | } 74 | ], 75 | "volumes": [ 76 | { 77 | "name": "default-token-rds7b", 78 | "secret": { 79 | "defaultMode": 420, 80 | "secretName": "default-token-rds7b" 81 | } 82 | } 83 | ] 84 | }, 85 | "status": { 86 | "conditions": [ 87 | { 88 | "lastProbeTime": null, 89 | "lastTransitionTime": "2020-04-16T08:50:12Z", 90 | "status": "True", 91 | "type": "Initialized" 92 | }, 93 | { 94 | "lastProbeTime": null, 95 | "lastTransitionTime": "2020-04-16T08:50:15Z", 96 | "status": "True", 97 | "type": "Ready" 98 | }, 99 | { 100 | "lastProbeTime": null, 101 | "lastTransitionTime": "2020-04-16T08:50:15Z", 102 | "status": "True", 103 | "type": "ContainersReady" 104 | }, 105 | { 106 | "lastProbeTime": null, 107 | "lastTransitionTime": "2020-04-16T08:50:12Z", 108 | "status": "True", 109 | "type": "PodScheduled" 110 | } 111 | ], 112 | "containerStatuses": [ 113 | { 114 | "containerID": "docker://698e8c5c83ac06e99d2cb8565e49585de7611fa2bff4c7b201330ea5c82f89b0", 115 | "image": "securesystemsengineering/sample-san-sama:hai", 116 | "imageID": "docker-pullable://securesystemsengineering/sample-san-sama@sha256:9191919ca86a88f0d42ce2eada3c7c545b92be5ee7fb1f23a561e814dc78b8c2", 117 | "lastState": {}, 118 | "name": "test-connaisseur", 119 | "ready": true, 120 | "restartCount": 0, 121 | "started": true, 122 | "state": { 123 | "running": { 124 | "startedAt": "2020-04-16T08:50:15Z" 125 | } 126 | } 127 | } 128 | ], 129 | "hostIP": "10.0.2.15", 130 | "phase": "Running", 131 | "podIP": "172.17.0.16", 132 | "podIPs": [ 133 | { 134 | "ip": "172.17.0.16" 135 | } 136 | ], 137 | "qosClass": "BestEffort", 138 | "startTime": "2020-04-16T08:50:12Z" 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /tests/data/sample_k8s_resources/replicasets.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "apps/v1", 3 | "kind": "ReplicaSet", 4 | "metadata": { 5 | "annotations": { 6 | "deployment.kubernetes.io/desired-replicas": "1", 7 | "deployment.kubernetes.io/max-replicas": "2", 8 | "deployment.kubernetes.io/revision": "1" 9 | }, 10 | "creationTimestamp": "2020-04-16T08:50:11Z", 11 | "generation": 1, 12 | "labels": { 13 | "app": "test-connaisseur", 14 | "pod-template-hash": "84d95bbc48" 15 | }, 16 | "name": "sample-san-sama-deployment-84d95bbc48", 17 | "namespace": "test-connaisseur", 18 | "ownerReferences": [ 19 | { 20 | "apiVersion": "apps/v1", 21 | "blockOwnerDeletion": true, 22 | "controller": true, 23 | "kind": "Deployment", 24 | "name": "sample-san-sama-deployment", 25 | "uid": "bff72959-9ed2-46bb-bbcf-ef34ca25e573" 26 | } 27 | ], 28 | "resourceVersion": "974168", 29 | "selfLink": "/apis/apps/v1/namespaces/test-connaisseur/replicasets/sample-san-sama-deployment-84d95bbc48", 30 | "uid": "090d26f8-1812-11ea-b3fc-02897404852e" 31 | }, 32 | "spec": { 33 | "replicas": 1, 34 | "selector": { 35 | "matchLabels": { 36 | "app": "test-connaisseur", 37 | "pod-template-hash": "84d95bbc48" 38 | } 39 | }, 40 | "template": { 41 | "metadata": { 42 | "creationTimestamp": null, 43 | "labels": { 44 | "app": "test-connaisseur", 45 | "pod-template-hash": "84d95bbc48" 46 | } 47 | }, 48 | "spec": { 49 | "containers": [ 50 | { 51 | "image": "securesystemsengineering/charlie-image@sha256:91ac9b26df583762234c1cdb2fc930364754ccc59bc752a2bfe298d2ea68f9ff", 52 | "imagePullPolicy": "Always", 53 | "name": "test-connaisseur", 54 | "ports": [ 55 | { 56 | "containerPort": 5000, 57 | "protocol": "TCP" 58 | } 59 | ], 60 | "resources": {}, 61 | "terminationMessagePath": "/dev/termination-log", 62 | "terminationMessagePolicy": "File" 63 | } 64 | ], 65 | "dnsPolicy": "ClusterFirst", 66 | "restartPolicy": "Always", 67 | "schedulerName": "default-scheduler", 68 | "securityContext": {}, 69 | "terminationGracePeriodSeconds": 30 70 | } 71 | } 72 | }, 73 | "status": { 74 | "availableReplicas": 1, 75 | "fullyLabeledReplicas": 1, 76 | "observedGeneration": 1, 77 | "readyReplicas": 1, 78 | "replicas": 1 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tests/data/scanfile_nosc_pod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | annotations: 5 | kubectl.kubernetes.io/last-applied-configuration: '{"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"labels":{"use":"semgr8s-integration-test"},"name":"nosc-pod","namespace":"validatedns"},"spec":{"containers":[{"command":["/bin/sh","-ec","sleep 6 | 1000"],"image":"busybox","name":"nosc-pod"}]}} 7 | 8 | ' 9 | creationTimestamp: '2024-02-12T23:18:38Z' 10 | labels: 11 | use: semgr8s-integration-test 12 | managedFields: 13 | - apiVersion: v1 14 | fieldsType: FieldsV1 15 | fieldsV1: 16 | f:metadata: 17 | f:annotations: 18 | .: {} 19 | f:kubectl.kubernetes.io/last-applied-configuration: {} 20 | f:labels: 21 | .: {} 22 | f:use: {} 23 | f:spec: 24 | f:containers: 25 | k:{"name":"nosc-pod"}: 26 | .: {} 27 | f:command: {} 28 | f:image: {} 29 | f:imagePullPolicy: {} 30 | f:name: {} 31 | f:resources: {} 32 | f:terminationMessagePath: {} 33 | f:terminationMessagePolicy: {} 34 | f:dnsPolicy: {} 35 | f:enableServiceLinks: {} 36 | f:restartPolicy: {} 37 | f:schedulerName: {} 38 | f:securityContext: {} 39 | f:terminationGracePeriodSeconds: {} 40 | manager: kubectl-client-side-apply 41 | operation: Update 42 | time: '2024-02-12T23:18:38Z' 43 | name: nosc-pod 44 | namespace: validatedns 45 | uid: e93043d3-8855-4c8c-a1ba-8df7cffd6a7f 46 | spec: 47 | containers: 48 | - command: 49 | - /bin/sh 50 | - -ec 51 | - sleep 1000 52 | image: busybox 53 | imagePullPolicy: Always 54 | name: nosc-pod 55 | resources: {} 56 | terminationMessagePath: /dev/termination-log 57 | terminationMessagePolicy: File 58 | volumeMounts: 59 | - mountPath: /var/run/secrets/kubernetes.io/serviceaccount 60 | name: kube-api-access-mlm8k 61 | readOnly: true 62 | dnsPolicy: ClusterFirst 63 | enableServiceLinks: true 64 | preemptionPolicy: PreemptLowerPriority 65 | priority: 0 66 | restartPolicy: Always 67 | schedulerName: default-scheduler 68 | securityContext: {} 69 | serviceAccount: default 70 | serviceAccountName: default 71 | terminationGracePeriodSeconds: 30 72 | tolerations: 73 | - effect: NoExecute 74 | key: node.kubernetes.io/not-ready 75 | operator: Exists 76 | tolerationSeconds: 300 77 | - effect: NoExecute 78 | key: node.kubernetes.io/unreachable 79 | operator: Exists 80 | tolerationSeconds: 300 81 | volumes: 82 | - name: kube-api-access-mlm8k 83 | projected: 84 | defaultMode: 420 85 | sources: 86 | - serviceAccountToken: 87 | expirationSeconds: 3607 88 | path: token 89 | - configMap: 90 | items: 91 | - key: ca.crt 92 | path: ca.crt 93 | name: kube-root-ca.crt 94 | - downwardAPI: 95 | items: 96 | - fieldRef: 97 | apiVersion: v1 98 | fieldPath: metadata.namespace 99 | path: namespace 100 | status: 101 | phase: Pending 102 | qosClass: BestEffort 103 | -------------------------------------------------------------------------------- /tests/demo/00_test-namespace.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: test-semgr8s 6 | labels: 7 | semgr8s/validation: enabled 8 | -------------------------------------------------------------------------------- /tests/demo/20_passing-deployment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: passing-testpod-1 6 | namespace: test-semgr8s 7 | spec: 8 | containers: 9 | - image: busybox 10 | name: passing-testpod-1 11 | command: ["/bin/sh", "-ec", "sleep 1000"] 12 | securityContext: 13 | allowPrivilegeEscalation: false 14 | capabilities: 15 | drop: 16 | - ALL 17 | privileged: false 18 | readOnlyRootFilesystem: true 19 | runAsNonRoot: true 20 | runAsUser: 10001 # remove when using openshift or OKD 4 21 | runAsGroup: 20001 # remove when using openshift or OKD 4 22 | seccompProfile: # remove when using Kubernetes prior v1.19, openshift or OKD 4 23 | type: RuntimeDefault # remove when using Kubernetes prior v1.19, openshift or OKD 4 24 | -------------------------------------------------------------------------------- /tests/demo/40_failing-deployment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: forbiddenlabel-pod 6 | namespace: test-semgr8s 7 | labels: 8 | foo: bar 9 | semgr8s-test: forbidden-test-label-e3b0c44298fc1c 10 | spec: 11 | containers: 12 | - image: busybox 13 | name: forbiddenlabel-container 14 | command: ["/bin/sh", "-ec", "sleep 1000"] 15 | securityContext: 16 | allowPrivilegeEscalation: false 17 | capabilities: 18 | drop: 19 | - ALL 20 | privileged: false 21 | readOnlyRootFilesystem: true 22 | runAsNonRoot: true 23 | runAsUser: 10001 # remove when using openshift or OKD 4 24 | runAsGroup: 20001 # remove when using openshift or OKD 4 25 | seccompProfile: # remove when using Kubernetes prior v1.19, openshift or OKD 4 26 | type: RuntimeDefault # remove when using Kubernetes prior v1.19, openshift or OKD 4 27 | --- 28 | apiVersion: v1 29 | kind: Pod 30 | metadata: 31 | name: failing-testpod-1 32 | namespace: test-semgr8s 33 | spec: 34 | containers: 35 | - image: busybox 36 | name: failing-testpod-1 37 | command: ["/bin/sh", "-ec", "sleep 1000"] 38 | --- 39 | apiVersion: v1 40 | kind: Pod 41 | metadata: 42 | name: failing-testpod-2 43 | namespace: test-semgr8s 44 | spec: 45 | containers: 46 | - image: busybox 47 | name: failing-testpod-2 48 | command: ["/bin/sh", "-ec", "sleep 1000"] 49 | securityContext: 50 | allowPrivilegeEscalation: true 51 | capabilities: 52 | drop: 53 | - ALL 54 | privileged: true 55 | readOnlyRootFilesystem: true 56 | runAsNonRoot: true 57 | --- 58 | apiVersion: v1 59 | kind: Pod 60 | metadata: 61 | name: failing-testpod-3 62 | namespace: test-semgr8s 63 | spec: 64 | containers: 65 | - image: busybox 66 | name: failing-testpod-3 67 | command: ["/bin/sh", "-ec", "sleep 1000"] 68 | securityContext: 69 | allowPrivilegeEscalation: false 70 | capabilities: 71 | drop: 72 | - ALL 73 | privileged: false 74 | readOnlyRootFilesystem: true 75 | hostNetwork: true 76 | -------------------------------------------------------------------------------- /tests/integration/README.md: -------------------------------------------------------------------------------- 1 | # Run integration tests 2 | 3 | Use the cluster of your choice, e.g. [kind](https://kind.sigs.k8s.io/). 4 | Specify which semgr8s image is to be used as environment variable, e.g.: 5 | 6 | ```bash 7 | export IMAGE=ghcr.io/semgr8ns/semgr8s 8 | export TAG=v0.1.0 9 | ``` 10 | 11 | Run the desired integration test via: 12 | ```bash 13 | tests/integration/main.sh "basic" 14 | ``` 15 | -------------------------------------------------------------------------------- /tests/integration/data/00_test_namespaces.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: validatedns 6 | labels: 7 | semgr8s/validation: enabled 8 | use: semgr8s-integration-test 9 | --- 10 | apiVersion: v1 11 | kind: Namespace 12 | metadata: 13 | name: ignoredns 14 | labels: 15 | use: semgr8s-integration-test 16 | -------------------------------------------------------------------------------- /tests/integration/data/01_semgr8ns_namespace.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: semgr8ns 6 | labels: 7 | kubernetes.io/metadata.name: semgr8ns 8 | -------------------------------------------------------------------------------- /tests/integration/data/20_compliant_pod.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: compliant-pod 6 | labels: 7 | use: semgr8s-integration-test 8 | spec: 9 | containers: 10 | - image: busybox 11 | name: compliant-pod 12 | command: ["/bin/sh", "-ec", "sleep 1000"] 13 | securityContext: 14 | allowPrivilegeEscalation: false 15 | capabilities: 16 | drop: 17 | - ALL 18 | privileged: false 19 | readOnlyRootFilesystem: true 20 | runAsNonRoot: true 21 | runAsUser: 10001 # remove when using openshift or OKD 4 22 | runAsGroup: 20001 # remove when using openshift or OKD 4 23 | seccompProfile: # remove when using Kubernetes prior v1.19, openshift or OKD 4 24 | type: RuntimeDefault # remove when using Kubernetes prior v1.19, openshift or OKD 4 25 | -------------------------------------------------------------------------------- /tests/integration/data/30_testlabel_pod.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: fixable-pod 6 | labels: 7 | use: semgr8s-integration-test 8 | semgr8s-test: forbidden-test-label-e3b0c44298fc1c 9 | spec: 10 | containers: 11 | - image: busybox 12 | name: fixable-container 13 | command: ["/bin/sh", "-ec", "sleep 1000"] 14 | securityContext: 15 | allowPrivilegeEscalation: false 16 | capabilities: 17 | drop: 18 | - ALL 19 | privileged: false 20 | readOnlyRootFilesystem: true 21 | runAsNonRoot: true 22 | runAsUser: 10001 # remove when using openshift or OKD 4 23 | runAsGroup: 20001 # remove when using openshift or OKD 4 24 | seccompProfile: # remove when using Kubernetes prior v1.19, openshift or OKD 4 25 | type: RuntimeDefault # remove when using Kubernetes prior v1.19, openshift or OKD 4 26 | -------------------------------------------------------------------------------- /tests/integration/data/40_testlabel_pod.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: forbiddenlabel-pod 6 | labels: 7 | use: semgr8s-integration-test 8 | foo: bar 9 | semgr8s-test: forbidden-test-label-e3b0c44298fc1c 10 | spec: 11 | containers: 12 | - image: busybox 13 | name: forbiddenlabel-container 14 | command: ["/bin/sh", "-ec", "sleep 1000"] 15 | securityContext: 16 | allowPrivilegeEscalation: false 17 | capabilities: 18 | drop: 19 | - ALL 20 | privileged: false 21 | readOnlyRootFilesystem: true 22 | runAsNonRoot: true 23 | runAsUser: 10001 # remove when using openshift or OKD 4 24 | runAsGroup: 20001 # remove when using openshift or OKD 4 25 | seccompProfile: # remove when using Kubernetes prior v1.19, openshift or OKD 4 26 | type: RuntimeDefault # remove when using Kubernetes prior v1.19, openshift or OKD 4 27 | -------------------------------------------------------------------------------- /tests/integration/data/41_nosc_pod.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: nosc-pod 6 | labels: 7 | use: semgr8s-integration-test 8 | spec: 9 | containers: 10 | - image: busybox 11 | name: nosc-pod 12 | command: ["/bin/sh", "-ec", "sleep 1000"] 13 | -------------------------------------------------------------------------------- /tests/integration/data/42_privileged_pod.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: privileged-pod 6 | labels: 7 | use: semgr8s-integration-test 8 | spec: 9 | containers: 10 | - image: busybox 11 | name: privileged-pod 12 | command: ["/bin/sh", "-ec", "sleep 1000"] 13 | securityContext: 14 | allowPrivilegeEscalation: true 15 | capabilities: 16 | drop: 17 | - ALL 18 | privileged: true 19 | readOnlyRootFilesystem: true 20 | runAsNonRoot: true 21 | -------------------------------------------------------------------------------- /tests/integration/data/43_hostnetwork_pod.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: hostnetwork-pod 6 | labels: 7 | use: semgr8s-integration-test 8 | spec: 9 | containers: 10 | - image: busybox 11 | name: hostnetwork-pod 12 | command: ["/bin/sh", "-ec", "sleep 1000"] 13 | securityContext: 14 | allowPrivilegeEscalation: false 15 | capabilities: 16 | drop: 17 | - ALL 18 | privileged: false 19 | readOnlyRootFilesystem: true 20 | hostNetwork: true 21 | -------------------------------------------------------------------------------- /tests/integration/data/44_multifail_pod.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: multifail-pod 6 | labels: 7 | use: semgr8s-integration-test 8 | semgr8s-test: forbidden-test-label-e3b0c44298fc1c 9 | foo: bar 10 | spec: 11 | containers: 12 | - image: busybox 13 | name: multifail-container 14 | command: ["/bin/sh", "-ec", "sleep 1000"] 15 | securityContext: 16 | allowPrivilegeEscalation: true 17 | capabilities: 18 | drop: 19 | - ALL 20 | privileged: true 21 | readOnlyRootFilesystem: true 22 | hostNetwork: true 23 | -------------------------------------------------------------------------------- /tests/integration/data/45_other_testlabel_pod.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: forbiddenlabel-pod 6 | labels: 7 | use: semgr8s-integration-test 8 | foo: bar 9 | semgr8s-test: other-forbidden-test-label-a948904f2f0f47 10 | spec: 11 | containers: 12 | - image: busybox 13 | name: forbiddenlabel-container 14 | command: ["/bin/sh", "-ec", "sleep 1000"] 15 | -------------------------------------------------------------------------------- /tests/integration/main.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | declare -A DEPLOYMENT_RES=(["VALID"]="0" ["INVALID"]="0") 5 | SCRIPT_PATH=$(tmp=$(realpath "$0") && dirname "${tmp}") 6 | RED="\033[0;31m" 7 | GREEN="\033[0;32m" 8 | NC="\033[0m" 9 | SUCCESS="${GREEN}SUCCESS${NC}" 10 | FAILED="${RED}FAILED${NC}" 11 | EXIT="0" 12 | RETRY=3 13 | 14 | # install/uninstall/upgrade, utility stuff 15 | source ${SCRIPT_PATH}/scripts/common.sh 16 | 17 | # integration test specific functions 18 | source "${SCRIPT_PATH}/scripts/basic.sh" 19 | source "${SCRIPT_PATH}/scripts/remote_rules.sh" 20 | source "${SCRIPT_PATH}/scripts/autofix.sh" 21 | source "${SCRIPT_PATH}/scripts/semgrep_login.sh" 22 | source "${SCRIPT_PATH}/scripts/audit.sh" 23 | 24 | # backup values.yaml 25 | cp charts/semgr8s/values.yaml charts/semgr8s/values.yaml.bak 26 | 27 | case $1 in 28 | "basic") 29 | # testing basic functionality 30 | basic_integration_test 31 | ;; 32 | "remote_rules") 33 | # testing multiple pre-built rules 34 | rules_integration_test 35 | ;; 36 | "autofix") 37 | # testing autofixing with mutating webhook 38 | autofix_integration_test 39 | ;; 40 | "semgrep_login") 41 | # testing semgrep appsec platform login with private rules 42 | semgrep_login_integration_test 43 | ;; 44 | "audit") 45 | # testing autofixing with mutating webhook 46 | audit_integration_test 47 | ;; 48 | "restore") 49 | restore 50 | ;; 51 | *) 52 | echo "Unknown test type: $1" 53 | EXIT="1" 54 | ;; 55 | esac 56 | 57 | if [[ "${EXIT}" != "0" ]]; then 58 | echo -e "${FAILED} Failed integration test." 59 | else 60 | echo -e "${SUCCESS} Passed integration test." 61 | fi 62 | 63 | if [[ "${CI-}" == "true" ]]; then 64 | exit $((${EXIT})) 65 | fi 66 | 67 | echo 'Cleaning up ...' 68 | restore 69 | make uninstall >/dev/null 2>&1 || true 70 | kubectl delete all,cronjobs,daemonsets,jobs,replicationcontrollers,statefulsets,namespaces -luse="semgr8s-integration-test" -A >/dev/null 71 | -------------------------------------------------------------------------------- /tests/integration/rules/test-semgr8s-no-foobar-label.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | - id: test-semgr8s-no-foobar-label 3 | patterns: 4 | - pattern-inside: | 5 | metadata: 6 | ... 7 | - pattern-inside: | 8 | labels: 9 | ... 10 | - pattern: | 11 | foo: bar 12 | message: TEST ONLY. Found kubernetes resource with semgr8s forbidden test label. Any resource with label "foo=bar" is denied. This label carries no meaning beyond testing and demonstration purposes. 13 | metadata: 14 | category: test 15 | technology: 16 | - kubernetes 17 | references: 18 | - https://semgr8ns.github.io/semgr8s/latest/#testing 19 | languages: [yaml] 20 | severity: INFO 21 | -------------------------------------------------------------------------------- /tests/integration/scripts/audit.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | audit_integration_test() { 5 | create_test_namespaces 6 | update_with_file "audit" 7 | install "make" 8 | multi_test "audit" 9 | uninstall "make" 10 | } 11 | -------------------------------------------------------------------------------- /tests/integration/scripts/autofix.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | autofix_integration_test() { 5 | add_rule "test-semgr8s-no-foobar-label" 6 | create_test_namespaces 7 | update_with_file "autofix" 8 | install "make" 9 | multi_test "autofix" 10 | uninstall "make" 11 | } 12 | -------------------------------------------------------------------------------- /tests/integration/scripts/basic.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | basic_integration_test() { 5 | create_test_namespaces 6 | update_with_file "basic" 7 | install "make" 8 | multi_test "basic" 9 | uninstall "make" 10 | } 11 | -------------------------------------------------------------------------------- /tests/integration/scripts/common.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | ## UTILS ------------------------------------------------------- ## 5 | fail() { 6 | echo -e "${FAILED}" 7 | exit 1 8 | } 9 | 10 | success() { 11 | echo -e "${SUCCESS}" 12 | } 13 | 14 | restore() { 15 | cp charts/semgr8s/values.yaml.bak charts/semgr8s/values.yaml 16 | rm charts/semgr8s/values.yaml.bak 17 | } 18 | 19 | null_to_empty() { 20 | read in 21 | 22 | if [[ "$in" == "null" ]]; then 23 | echo "" 24 | else 25 | echo "$in" 26 | fi 27 | } 28 | 29 | ## INSTALLATIONS ---------------------------------------------- ## 30 | install() { # $1: helm or make, $2: namespace (helm), $3: additional args (helm) 31 | echo -n 'Installing semgr8s ... ' 32 | case $1 in 33 | "helm") 34 | helm install semgr8s charts/semgr8s --atomic --namespace "${2}" \ 35 | ${3} >/dev/null || fail 36 | sleep 1 37 | ;; 38 | "make") 39 | make install >/dev/null || fail 40 | sleep 1 41 | ;; 42 | *) 43 | fail 44 | ;; 45 | esac 46 | success 47 | } 48 | 49 | uninstall() { # $1: helm or make, $2: namespace (helm) 50 | echo -n 'Uninstalling semgr8s ...' 51 | case $1 in 52 | "helm") 53 | helm uninstall semgr8s --namespace "${2}" >/dev/null || fail 54 | ;; 55 | "make") 56 | make uninstall >/dev/null || fail 57 | ;; 58 | "force") 59 | kubectl delete all,secrets,serviceaccounts,mutatingwebhookconfigurations,configmaps,namespaces \ 60 | -lapp.kubernetes.io/instance=semgr8s -A --force --grace-period=0 >/dev/null 2>&1 61 | ;; 62 | *) 63 | fail 64 | ;; 65 | esac 66 | success 67 | } 68 | 69 | upgrade() { # $1: helm or make, $2: namespace (helm) 70 | echo -n 'Upgrading semgr8s ...' 71 | case $1 in 72 | "helm") 73 | helm upgrade semgr8s charts/semgr8s --wait \ 74 | --namespace "${2}" >/dev/null || fail 75 | ;; 76 | "make") 77 | make upgrade >/dev/null || fail 78 | ;; 79 | *) 80 | fail 81 | ;; 82 | esac 83 | success 84 | } 85 | 86 | ## UPDATES ----------------------------------------------------- ## 87 | 88 | update() { # $@: update expressions 89 | for update in "$@"; do 90 | yq e -i "${update}" charts/semgr8s/values.yaml 91 | done 92 | } 93 | 94 | update_with_file() { # $1: file name 95 | envsubst $1 96 | yq eval-all --inplace 'select(fileIndex == 0) * select(fileIndex == 1).values' charts/semgr8s/values.yaml $1 97 | rm $1 98 | } 99 | 100 | ## RULES ------------------------------------------------------- ## 101 | 102 | add_rule() { # $1: rule name 103 | cp tests/integration/rules/$1.yaml charts/semgr8s/rules/ 104 | } 105 | 106 | add_test_rules() { 107 | cp tests/integration/rules/* charts/semgr8s/rules/ 108 | } 109 | 110 | ## NAMESPACES --------------------------------------------- ## 111 | 112 | create_semgr8ns_namespace() { 113 | echo -n "Creating semgr8ns namespace..." 114 | kubectl apply -f tests/integration/data/01_semgr8ns_namespace.yaml >/dev/null 115 | success 116 | } 117 | 118 | create_test_namespaces() { 119 | echo -n "Creating test namespaces..." 120 | kubectl apply -f tests/integration/data/00_test_namespaces.yaml >/dev/null 121 | success 122 | } 123 | 124 | ## TESTS ------------------------------------------------------- ## 125 | single_test() { # ID TXT TYP REF NS MSG RES 126 | echo -n "[$1] $2" 127 | i=0 # intialize iterator 128 | export RAND=$(head -c 5 /dev/urandom | hexdump -ve '1/1 "%.2x"') # creating a random index to label the pods and avoid name collision for repeated runs 129 | 130 | if [[ "$6" == "" ]]; then 131 | MSG="pod/pod-$1-${RAND} created" 132 | else 133 | MSG=$(envsubst <<<"$6") # in case RAND is to be used, it needs to be added as ${RAND} to cases.yaml (and maybe deployment file) 134 | fi 135 | 136 | while :; do 137 | i=$((i + 1)) 138 | if [[ "$3" == "deploy" ]]; then 139 | kubectl run pod-$1-${RAND} --image="$4" --namespace="$5" -luse="semgr8s-integration-test" >output.log 2>&1 || true 140 | else 141 | kubectl apply -f "${SCRIPT_PATH}/data/$4.yaml" --namespace="$5" >output.log 2>&1 || true 142 | fi 143 | # if the webhook couldn't be called, try again. 144 | [[ ("$(cat output.log)" =~ "failed calling webhook") && $i -lt ${RETRY} ]] || break 145 | done 146 | if [[ ! "$(cat output.log)" =~ "${MSG}" ]]; then 147 | echo -e ${FAILED} 148 | echo "::group::Output" 149 | cat output.log 150 | kubectl logs -n semgr8ns -lapp.kubernetes.io/instance=semgr8s 151 | echo "::endgroup::" 152 | EXIT="1" 153 | else 154 | echo -e "${SUCCESS}" 155 | fi 156 | rm output.log 157 | 158 | if [[ "$7" != "null" ]]; then 159 | DEPLOYMENT_RES[$7]=$((${DEPLOYMENT_RES[$7]} + 1)) 160 | fi 161 | 162 | # 3 tries on first test, 2 tries on second, 1 try for all subsequential 163 | RETRY=$((RETRY - 1)) 164 | } 165 | 166 | multi_test() { # $1: file name, $2: key to find the testcases (default: testCases) 167 | 168 | # converting to json, as yq processing is pretty slow 169 | test_cases=$(yq e -o=json ".${2:-testCases}" ${SCRIPT_PATH}/test_cases/$1.yaml) 170 | len=$(echo ${test_cases} | jq 'length') 171 | for i in $(seq 0 $(($len - 1))); do 172 | test_case=$(echo ${test_cases} | jq ".[$i]") 173 | ID=$(echo ${test_case} | jq -r ".id" | null_to_empty) 174 | TEST_CASE_TXT=$(echo ${test_case} | jq -r ".txt" | null_to_empty) 175 | TYPE=$(echo ${test_case} | jq -r ".type" | null_to_empty) 176 | REF=$(echo ${test_case} | jq -r ".ref" | null_to_empty) 177 | NAMESPACE=$(echo ${test_case} | jq -r ".namespace" | null_to_empty) 178 | EXP_MSG=$(echo ${test_case} | jq -r ".expected_msg" | null_to_empty) 179 | EXP_RES=$(echo ${test_case} | jq -r ".expected_result" | null_to_empty) 180 | single_test "${ID}" "${TEST_CASE_TXT}" "${TYPE:=deploy}" "${REF}" "${NAMESPACE:=default}" "${EXP_MSG}" "${EXP_RES:=null}" 181 | done 182 | } 183 | -------------------------------------------------------------------------------- /tests/integration/scripts/remote_rules.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | rules_integration_test() { 5 | create_test_namespaces 6 | update_with_file "basic" 7 | install "make" 8 | multi_test "remote_rules" 9 | uninstall "make" 10 | } 11 | -------------------------------------------------------------------------------- /tests/integration/scripts/semgrep_login.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | semgrep_login_integration_test() { 5 | create_semgr8ns_namespace 6 | kubectl create secret generic -n semgr8ns --from-literal=token="${SEMGREP_APP_TOKEN}" semgrep-app-token 7 | create_test_namespaces 8 | update_with_file "semgrep_login" 9 | install "make" 10 | multi_test "semgrep_login" 11 | uninstall "make" 12 | } 13 | -------------------------------------------------------------------------------- /tests/integration/test_cases/audit.yaml: -------------------------------------------------------------------------------- 1 | testCases: 2 | - id: a-01 3 | txt: Testing compliant pod... 4 | type: k8s-yaml 5 | ref: 20_compliant_pod 6 | namespace: validatedns 7 | expected_msg: pod/compliant-pod created 8 | - id: a-02 9 | txt: Testing non-compliant pod against local rule for forbidden test label... 10 | type: k8s-yaml 11 | ref: 40_testlabel_pod 12 | namespace: validatedns 13 | expected_msg: "Warning: [Semgr8s] Found 1 violation" 14 | - id: a-03 15 | txt: Testing non-compliant pod against local rule for forbidden test label... 16 | type: k8s-yaml 17 | ref: 41_nosc_pod 18 | namespace: validatedns 19 | expected_msg: pod/nosc-pod created 20 | 21 | values: 22 | deployment: 23 | image: 24 | repository: "${IMAGE}" 25 | tag: "${TAG}" 26 | application: 27 | enforce: false 28 | -------------------------------------------------------------------------------- /tests/integration/test_cases/autofix.yaml: -------------------------------------------------------------------------------- 1 | testCases: 2 | - id: a-01 3 | txt: Testing compliant pod... 4 | type: k8s-yaml 5 | ref: 20_compliant_pod 6 | namespace: validatedns 7 | expected_msg: pod/compliant-pod created 8 | - id: a-02 9 | txt: Testing non-compliant in ignored namespace... 10 | type: k8s-yaml 11 | ref: 40_testlabel_pod 12 | namespace: ignoredns 13 | expected_msg: pod/forbiddenlabel-pod created 14 | - id: a-03 15 | txt: Testing non-compliant partly autofixable pod... 16 | type: k8s-yaml 17 | ref: 40_testlabel_pod 18 | namespace: validatedns 19 | expected_msg: rules.test-semgr8s-no-foobar-label 20 | - id: a-04 21 | txt: Testing non-compliant autofixable pod... 22 | type: k8s-yaml 23 | ref: 30_testlabel_pod 24 | namespace: validatedns 25 | expected_msg: pod/fixable-pod created 26 | 27 | values: 28 | deployment: 29 | image: 30 | repository: "${IMAGE}" 31 | tag: "${TAG}" 32 | application: 33 | autofix: true 34 | -------------------------------------------------------------------------------- /tests/integration/test_cases/basic.yaml: -------------------------------------------------------------------------------- 1 | testCases: 2 | - id: b-01 3 | txt: Testing compliant pod... 4 | type: k8s-yaml 5 | ref: 20_compliant_pod 6 | namespace: validatedns 7 | expected_msg: pod/compliant-pod created 8 | - id: b-02 9 | txt: Testing non-compliant in ignored namespace... 10 | type: k8s-yaml 11 | ref: 40_testlabel_pod 12 | namespace: ignoredns 13 | expected_msg: pod/forbiddenlabel-pod created 14 | - id: b-03 15 | txt: Testing non-compliant pod against local rule for forbidden test label... 16 | type: k8s-yaml 17 | ref: 40_testlabel_pod 18 | namespace: validatedns 19 | expected_msg: rules.test-semgr8s-forbidden-label 20 | 21 | values: 22 | deployment: 23 | image: 24 | repository: "${IMAGE}" 25 | tag: "${TAG}" 26 | -------------------------------------------------------------------------------- /tests/integration/test_cases/remote_rules.yaml: -------------------------------------------------------------------------------- 1 | testCases: 2 | - id: r-01 3 | txt: Testing compliant pod... 4 | type: k8s-yaml 5 | ref: 20_compliant_pod 6 | namespace: validatedns 7 | expected_msg: pod/compliant-pod created 8 | - id: r-02 9 | txt: Testing non-compliant pod w/o securityContext... 10 | type: k8s-yaml 11 | ref: 41_nosc_pod 12 | namespace: validatedns 13 | expected_msg: yaml.kubernetes.security.writable-filesystem-container.writable-filesystem-container 14 | - id: r-03 15 | txt: Testing non-compliant privileged pod... 16 | type: k8s-yaml 17 | ref: 42_privileged_pod 18 | namespace: validatedns 19 | expected_msg: yaml.kubernetes.security.privileged-container.privileged-container 20 | - id: r-04 21 | txt: Testing non-compliant pod w/ access to host network... 22 | type: k8s-yaml 23 | ref: 43_hostnetwork_pod 24 | namespace: validatedns 25 | expected_msg: yaml.kubernetes.security.hostnetwork-pod.hostnetwork-pod 26 | - id: r-05 27 | txt: Testing non-compliant pod w/ multiple local & remote issues... 28 | type: k8s-yaml 29 | ref: 44_multifail_pod 30 | namespace: validatedns 31 | expected_msg: Found 3 violation(s) of the following policies 32 | 33 | values: 34 | deployment: 35 | image: 36 | repository: "${IMAGE}" 37 | tag: "${TAG}" 38 | -------------------------------------------------------------------------------- /tests/integration/test_cases/semgrep_login.yaml: -------------------------------------------------------------------------------- 1 | testCases: 2 | - id: sl-01 3 | txt: Testing compliant pod... 4 | type: k8s-yaml 5 | ref: 20_compliant_pod 6 | namespace: validatedns 7 | expected_msg: pod/compliant-pod created 8 | - id: sl-02 9 | txt: Testing non-compliant pod... 10 | type: k8s-yaml 11 | ref: 45_other_testlabel_pod 12 | namespace: validatedns 13 | expected_msg: k8s.test-other-semgr8s-forbidden-label 14 | 15 | values: 16 | deployment: 17 | image: 18 | repository: "${IMAGE}" 19 | tag: "${TAG}" 20 | application: 21 | remoteRules: ["r/k8s.test-other-semgr8s-forbidden-label"] 22 | semgrepLogin: true 23 | -------------------------------------------------------------------------------- /tests/test_app.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | import pytest 4 | 5 | from . import conftest as fix 6 | 7 | 8 | @pytest.fixture(autouse=True) 9 | def set_envs(monkeypatch): 10 | monkeypatch.setenv("SEMGREP_RULES", "") 11 | 12 | 13 | @pytest.fixture(autouse=True) 14 | def set_flask_app(monkeypatch): 15 | import semgr8s.app as sapp 16 | 17 | pytest.sapp = sapp 18 | 19 | 20 | def test_healthz(): 21 | client = pytest.sapp.APP.test_client() 22 | get_response = client.get("/health") 23 | post_response = client.post("/health") 24 | assert get_response.status_code == 200 25 | assert post_response.status_code == 200 26 | 27 | 28 | def test_readyz(): 29 | client = pytest.sapp.APP.test_client() 30 | get_response = client.get("/ready") 31 | post_response = client.post("/ready") 32 | assert get_response.status_code == 200 33 | assert post_response.status_code == 200 34 | 35 | 36 | @pytest.mark.parametrize( 37 | "index, remote_rules, allowed, code, msg", 38 | [ 39 | (5, "", True, 201, "Compliant resource admitted"), 40 | (0, "", False, 400, "no payload.request found"), 41 | (1, "", False, 400, "no payload.request found"), 42 | (6, "", False, 403, "forbidden-label"), 43 | (None, "", False, 415, "Unsupported Media Type"), 44 | (5, "foobar/random-e3b0c4", False, 418, "error during semgrep scan"), 45 | (2, "", False, 422, "no payload.request.uid found"), 46 | ], 47 | ) 48 | def test_validate( 49 | monkeypatch, adm_req_samples, index, remote_rules, allowed, code, msg 50 | ): 51 | monkeypatch.setenv("SEMGREP_RULES", remote_rules) 52 | client = pytest.sapp.APP.test_client() 53 | if index is not None: 54 | payload = adm_req_samples[index] 55 | else: 56 | payload = "" 57 | response = client.post("/validate", json=payload) 58 | assert response.is_json 59 | assert response.status_code == 200 60 | admission_response = response.get_json()["response"] 61 | assert admission_response["allowed"] == allowed 62 | assert admission_response["status"]["code"] == code 63 | assert msg in admission_response["status"]["message"] 64 | 65 | 66 | @pytest.mark.parametrize( 67 | "index, remote_rules, allowed, code, msg, patch", 68 | [ 69 | (5, "", True, 201, "Compliant resource admitted", None), 70 | ( 71 | 6, 72 | "", 73 | True, 74 | 202, 75 | "forbidden-label", 76 | '[{"op": "remove", "path": "/metadata/labels/semgr8s-test"}]', 77 | ), 78 | ], 79 | ) 80 | def test_mutate( 81 | monkeypatch, adm_req_samples, index, remote_rules, allowed, code, msg, patch 82 | ): 83 | monkeypatch.setenv("SEMGREP_RULES", remote_rules) 84 | client = pytest.sapp.APP.test_client() 85 | if index is not None: 86 | payload = adm_req_samples[index] 87 | else: 88 | payload = "" 89 | response = client.post("/mutate", json=payload) 90 | assert response.is_json 91 | assert response.status_code == 200 92 | admission_response = response.get_json()["response"] 93 | assert admission_response["allowed"] == allowed 94 | assert admission_response["status"]["code"] == code 95 | assert msg in admission_response["status"]["message"] 96 | if patch: 97 | assert admission_response["patchType"] == "JSONPatch" 98 | assert admission_response["patch"] == base64.b64encode( 99 | bytearray(json.dumps(json.loads(patch)), "utf-8") 100 | ).decode("utf-8") 101 | -------------------------------------------------------------------------------- /tests/test_k8s_api.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pytest 3 | 4 | import semgr8s.k8s_api as kapi 5 | 6 | from . import conftest as fix 7 | 8 | 9 | @pytest.mark.parametrize( 10 | "url, response, logtext", 11 | [ 12 | ( 13 | "https://samplek8s.io/apis/v1/namespaces/default/pods/sample-pod", 14 | fix.get_k8s_res("pods"), 15 | "", 16 | ), 17 | ( 18 | "https://samplek8s.io/apis/v1/namespaces/default/deployments/sample-dpl", 19 | fix.get_k8s_res("deployments"), 20 | "", 21 | ), 22 | ( 23 | "https://samplek8s.io/apis/v1/namespaces/broken_nojson/configmaps/sample-cm", 24 | {}, 25 | "Malformed k8s API response or resource yaml", 26 | ), 27 | ], 28 | ) 29 | def test_request_kube_api( 30 | monkeypatch, caplog, m_request, url: str, response: dict, logtext: str 31 | ): 32 | monkeypatch.setenv("KUBERNETES_SERVICE_HOST", "127.0.0.1") 33 | monkeypatch.setenv("KUBERNETES_SERVICE_PORT", "1234") 34 | assert kapi.request_kube_api(url) == response 35 | assert logtext in caplog.text 36 | -------------------------------------------------------------------------------- /tests/test_updater.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import pytest 4 | 5 | import semgr8s.updater as supdater 6 | 7 | from . import conftest as fix 8 | 9 | 10 | # BE CAREFUL files created in previous runs, remain accessible 11 | @pytest.mark.parametrize( 12 | "namespace, created_files, logtext", 13 | [ 14 | ( 15 | "default", 16 | ["tester-test-name.yaml"], 17 | "", 18 | ), 19 | ( 20 | "semgr8ns", 21 | ["tester-test-name1.yaml"], 22 | "", 23 | ), 24 | ( 25 | "broken_nodata", 26 | ["tester-test-broken.yaml"], 27 | "", 28 | ), 29 | ( 30 | "multiplerulesinmap", 31 | [ 32 | "tester-test-multiplerules1.yaml", 33 | "tester-test-multiplerules2.yaml", 34 | ], 35 | "", 36 | ), 37 | ], 38 | ) 39 | def test_updater(monkeypatch, caplog, m_request, namespace, created_files, logtext): 40 | monkeypatch.setenv("KUBERNETES_SERVICE_HOST", "127.0.0.1") 41 | monkeypatch.setenv("KUBERNETES_SERVICE_PORT", "1234") 42 | monkeypatch.setenv("NAMESPACE", namespace) 43 | supdater.update_rules() 44 | for f in created_files: 45 | assert f"{f}" in os.listdir("/app/rules/") 46 | assert logtext in caplog.text 47 | --------------------------------------------------------------------------------