├── .github └── workflows │ ├── release.yaml │ └── test.yaml ├── .gitignore ├── .manifest ├── LICENSE ├── Makefile ├── README.md ├── avd_docs ├── docker │ ├── AVD-DS-0001 │ │ └── docs.md │ ├── AVD-DS-0002 │ │ └── docs.md │ ├── AVD-DS-0004 │ │ └── docs.md │ ├── AVD-DS-0005 │ │ └── docs.md │ ├── AVD-DS-0006 │ │ └── docs.md │ ├── AVD-DS-0007 │ │ └── docs.md │ ├── AVD-DS-0008 │ │ └── docs.md │ ├── AVD-DS-0009 │ │ └── docs.md │ ├── AVD-DS-0010 │ │ └── docs.md │ ├── AVD-DS-0011 │ │ └── docs.md │ ├── AVD-DS-0012 │ │ └── docs.md │ ├── AVD-DS-0013 │ │ └── docs.md │ ├── AVD-DS-0014 │ │ └── docs.md │ ├── AVD-DS-0015 │ │ └── docs.md │ ├── AVD-DS-0016 │ │ └── docs.md │ ├── AVD-DS-0017 │ │ └── docs.md │ ├── AVD-DS-0018 │ │ └── docs.md │ ├── AVD-DS-0019 │ │ └── docs.md │ ├── AVD-DS-0020 │ │ └── docs.md │ ├── AVD-DS-0021 │ │ └── docs.md │ ├── AVD-DS-0022 │ │ └── docs.md │ ├── AVD-DS-0023 │ │ └── docs.md │ └── AVD-DS-0024 │ │ └── docs.md └── kubernetes │ ├── advanced │ ├── AVD-KSV-0004 │ │ └── docs.md │ ├── AVD-KSV-0007 │ │ └── docs.md │ ├── AVD-KSV-0032 │ │ └── docs.md │ ├── AVD-KSV-0033 │ │ └── docs.md │ ├── AVD-KSV-0034 │ │ └── docs.md │ ├── AVD-KSV-0035 │ │ └── docs.md │ ├── AVD-KSV-0036 │ │ └── docs.md │ ├── AVD-KSV-0037 │ │ └── docs.md │ └── AVD-KSV-0038 │ │ └── docs.md │ ├── general │ ├── AVD-KSV-0003 │ │ └── docs.md │ ├── AVD-KSV-0005 │ │ └── docs.md │ ├── AVD-KSV-0006 │ │ └── docs.md │ ├── AVD-KSV-0011 │ │ └── docs.md │ ├── AVD-KSV-0013 │ │ └── docs.md │ ├── AVD-KSV-0014 │ │ └── docs.md │ ├── AVD-KSV-0015 │ │ └── docs.md │ ├── AVD-KSV-0016 │ │ └── docs.md │ ├── AVD-KSV-0018 │ │ └── docs.md │ ├── AVD-KSV-0020 │ │ └── docs.md │ ├── AVD-KSV-0021 │ │ └── docs.md │ └── AVD-KSV-0102 │ │ └── docs.md │ └── pss │ ├── baseline │ ├── AVD-KSV-0002 │ │ └── docs.md │ ├── AVD-KSV-0008 │ │ └── docs.md │ ├── AVD-KSV-0009 │ │ └── docs.md │ ├── AVD-KSV-0010 │ │ └── docs.md │ ├── AVD-KSV-0017 │ │ └── docs.md │ ├── AVD-KSV-0022 │ │ └── docs.md │ ├── AVD-KSV-0023 │ │ └── docs.md │ ├── AVD-KSV-0024 │ │ └── docs.md │ ├── AVD-KSV-0025 │ │ └── docs.md │ ├── AVD-KSV-0026 │ │ └── docs.md │ └── AVD-KSV-0027 │ │ └── docs.md │ └── restricted │ ├── AVD-KSV-0001 │ └── docs.md │ ├── AVD-KSV-0012 │ └── docs.md │ ├── AVD-KSV-0028 │ └── docs.md │ ├── AVD-KSV-0029 │ └── docs.md │ └── AVD-KSV-0030 │ └── docs.md ├── docker ├── README.md ├── lib │ └── docker.rego ├── policies │ ├── README.md │ ├── add_instead_of_copy.rego │ ├── add_instead_of_copy_test.rego │ ├── apt_get_missing_yes_flag_to_avoid_manual_input.rego │ ├── apt_get_missing_yes_flag_to_avoid_manual_input_test.rego │ ├── copy_from_references_current_from_alias.rego │ ├── copy_from_references_current_from_alias_test.rego │ ├── copy_from_without_from_alias_defined_previously.rego │ ├── copy_from_without_from_alias_defined_previously_test.rego │ ├── copy_with_more_than_two_arguments_not_ending_with_slash.rego │ ├── copy_with_more_than_two_arguments_not_ending_with_slash_test.rego │ ├── index.yaml │ ├── latest_tag.rego │ ├── latest_tag_test.rego │ ├── maintainer_is_deprecated.rego │ ├── maintainer_is_deprecated_test.rego │ ├── missing_dnf_clean_all.rego │ ├── missing_dnf_clean_all_test.rego │ ├── missing_zypper_clean.rego │ ├── missing_zypper_clean_test.rego │ ├── multiple_cmd_instructions_listed.rego │ ├── multiple_cmd_instructions_listed_test.rego │ ├── multiple_entrypoint_instructions_listed.rego │ ├── multiple_entrypoint_instructions_listed_test.rego │ ├── multiple_healthcheck_instructions.rego │ ├── multiple_healthcheck_instructions_test.rego │ ├── port22.rego │ ├── port22_test.rego │ ├── root_user.rego │ ├── root_user_test.rego │ ├── run_apt_get_dist_upgrade.rego │ ├── run_apt_get_dist_upgrade_test.rego │ ├── run_command_cd_instead_of_workdir.rego │ ├── run_command_cd_instead_of_workdir_test.rego │ ├── run_using_sudo.rego │ ├── run_using_sudo_test.rego │ ├── run_using_wget_and_curl.rego │ ├── run_using_wget_and_curl_test.rego │ ├── same_alias_in_different_froms.rego │ ├── same_alias_in_different_froms_test.rego │ ├── unix_ports_out_of_range.rego │ ├── unix_ports_out_of_range_test.rego │ ├── update_instruction_alone.rego │ ├── update_instruction_alone_test.rego │ ├── workdir_path_not_absolute.rego │ ├── workdir_path_not_absolute_test.rego │ ├── yum_clean_all_missing.rego │ └── yum_clean_all_missing_test.rego └── test │ └── Dockerfile ├── go.mod ├── go.sum ├── integration ├── integration_test.go └── testdata │ ├── DS001 │ ├── Dockerfile.allowed │ └── Dockerfile.denied │ ├── DS002 │ ├── Dockerfile.allowed │ └── Dockerfile.denied │ ├── DS003 │ ├── Dockerfile.allowed │ └── Dockerfile.denied │ ├── DS004 │ ├── Dockerfile.allowed │ └── Dockerfile.denied │ ├── DS005 │ ├── Dockerfile.allowed │ └── Dockerfile.denied │ ├── DS006 │ ├── Dockerfile.allowed │ └── Dockerfile.denied │ ├── DS007 │ ├── Dockerfile.allowed │ └── Dockerfile.denied │ ├── DS008 │ ├── Dockerfile.allowed │ └── Dockerfile.denied │ ├── DS009 │ ├── Dockerfile.allowed │ └── Dockerfile.denied │ ├── DS010 │ ├── Dockerfile.allowed │ └── Dockerfile.denied │ ├── DS011 │ ├── Dockerfile.allowed │ └── Dockerfile.denied │ ├── DS012 │ ├── Dockerfile.allowed │ └── Dockerfile.denied │ ├── DS013 │ ├── Dockerfile.allowed │ └── Dockerfile.denied │ ├── DS014 │ ├── Dockerfile.allowed │ └── Dockerfile.denied │ ├── DS015 │ ├── Dockerfile.allowed │ └── Dockerfile.denied │ ├── DS016 │ ├── Dockerfile.allowed │ └── Dockerfile.denied │ ├── DS017 │ ├── Dockerfile.allowed │ └── Dockerfile.denied │ ├── DS018 │ ├── Dockerfile.allowed │ └── Dockerfile.denied │ ├── DS019 │ ├── Dockerfile.allowed │ └── Dockerfile.denied │ ├── DS020 │ ├── Dockerfile.allowed │ └── Dockerfile.denied │ ├── DS021 │ ├── Dockerfile.allowed │ └── Dockerfile.denied │ ├── DS022 │ ├── Dockerfile.allowed │ └── Dockerfile.denied │ ├── DS023 │ ├── Dockerfile.allowed │ └── Dockerfile.denied │ └── DS024 │ ├── Dockerfile.allowed │ └── Dockerfile.denied ├── kubernetes ├── README.md ├── lib │ ├── kubernetes.rego │ ├── kubernetes_test.rego │ └── utils.rego └── policies │ ├── advanced │ ├── capabilities_no_drop_at_least_one.rego │ ├── manages_etc_hosts.rego │ ├── protect_core_components_namespace.rego │ ├── protect_core_components_namespace_test.rego │ ├── protecting_pod_service_account_tokens.rego │ ├── protecting_pod_service_account_tokens_test.rego │ ├── selector_usage_in_network_policies.rego │ ├── selector_usage_in_network_policies_test.rego │ ├── uses_untrusted_azure_registry.rego │ ├── uses_untrusted_ecr_registry.rego │ ├── uses_untrusted_gcr_registry.rego │ └── uses_untrusted_public_registries.rego │ ├── general │ ├── CPU_not_limited.rego │ ├── CPU_not_limited_test.rego │ ├── CPU_requests_not_specified.rego │ ├── CPU_requests_not_specified_test.rego │ ├── SYS_ADMIN_capability.rego │ ├── SYS_ADMIN_capability_test.rego │ ├── capabilities_no_drop_all.rego │ ├── capabilities_no_drop_all_test.rego │ ├── deprecated_api_version.rego │ ├── deprecated_api_version_test.rego │ ├── file_system_not_read_only.rego │ ├── file_system_not_read_only_test.rego │ ├── memory_not_limited.rego │ ├── memory_not_limited_test.rego │ ├── memory_requests_not_specified.rego │ ├── memory_requests_not_specified_test.rego │ ├── mounts_docker_socket.rego │ ├── mounts_docker_socket_test.rego │ ├── runs_with_GID_le_10000.rego │ ├── runs_with_GID_le_10000_test.rego │ ├── runs_with_UID_le_10000.rego │ ├── runs_with_UID_le_10000_test.rego │ ├── tiller_is_deployed.rego │ ├── tiller_is_deployed_test.rego │ ├── uses_image_tag_latest.rego │ └── uses_image_tag_latest_test.rego │ └── pss │ ├── baseline │ ├── 1_host_ipc.rego │ ├── 1_host_ipc_test.rego │ ├── 1_host_network.rego │ ├── 1_host_network_test.rego │ ├── 1_host_pid.rego │ ├── 1_host_pid_test.rego │ ├── 2_privileged.rego │ ├── 2_privileged_test.rego │ ├── 3_specific_capabilities_added.rego │ ├── 3_specific_capabilities_added_test.rego │ ├── 4_hostpath_volumes_mounted.rego │ ├── 4_hostpath_volumes_mounted_test.rego │ ├── 5_access_to_host_ports.rego │ ├── 5_access_to_host_ports_test.rego │ ├── 6_apparmor_policy_disabled.rego │ ├── 6_apparmor_policy_disabled_test.rego │ ├── 7_selinux_custom_options_set.rego │ ├── 7_selinux_custom_options_set_test.rego │ ├── 8_non_default_proc_masks_set.rego │ ├── 8_non_default_proc_masks_set_test.rego │ ├── 9_unsafe_sysctl_options_set.rego │ └── 9_unsafe_sysctl_options_set_test.rego │ └── restricted │ ├── 1_non_core_volume_types.rego │ ├── 1_non_core_volume_types_test.rego │ ├── 2_can_elevate_its_own_privileges.rego │ ├── 2_can_elevate_its_own_privileges_test.rego │ ├── 3_runs_as_root.rego │ ├── 3_runs_as_root_test.rego │ ├── 4_runs_with_a_root_gid.rego │ ├── 4_runs_with_a_root_gid_test.rego │ ├── 5_runtime_default_seccomp_profile_not_set.rego │ └── 5_runtime_default_seccomp_profile_not_set_test.rego └── tools ├── avd_generator └── main.go ├── lint └── main.go └── rego ├── rego_files.go └── rego_metadata.go /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - "v*" 6 | env: 7 | GH_USER: aqua-bot 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | - name: Get the semantic version 15 | run: echo "RELEASE_VERSION=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_ENV 16 | - name: Get the minor version 17 | run: echo "MINOR_VERSION=$(echo ${RELEASE_VERSION} | cut -d. -f1,2)" >> $GITHUB_ENV 18 | - name: Get the major version 19 | run: echo "MAJOR_VERSION=$(echo ${RELEASE_VERSION} | cut -d. -f1)" >> $GITHUB_ENV 20 | - name: Copy Rego files 21 | run: rsync -avr --exclude=README.md --exclude="*_test.rego" --exclude=test --exclude=advanced docker kubernetes bundle/ 22 | - name: Copy manifest 23 | run: | 24 | cp .manifest bundle/ 25 | sed -i -e "s/\[GITHUB_SHA\]/${RELEASE_VERSION}/" bundle/.manifest 26 | - name: Compress 27 | run: tar -C bundle -czvf bundle.tar.gz . 28 | - name: Login to GitHub Packages Container registry 29 | uses: docker/login-action@v1 30 | with: 31 | registry: ghcr.io 32 | username: ${{ env.GH_USER }} 33 | password: ${{ secrets.GITHUB_TOKEN }} 34 | - name: Deploy to GitHub Packages Container registry 35 | run: | 36 | tags=(latest ${{ env.RELEASE_VERSION}} ${{env.MINOR_VERSION }} ${{ env.MAJOR_VERSION }}) 37 | for tag in ${tags[@]}; do 38 | oras push ghcr.io/${{ github.repository }}:${tag} \ 39 | --manifest-config /dev/null:application/vnd.cncf.openpolicyagent.config.v1+json \ 40 | bundle.tar.gz:application/vnd.cncf.openpolicyagent.layer.v1.tar+gzip 41 | done 42 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths-ignore: 7 | - '**/*.md' 8 | - 'LICENSE' 9 | pull_request: 10 | paths-ignore: 11 | - '**/*.md' 12 | - 'LICENSE' 13 | env: 14 | GO_VERSION: "1.16" 15 | jobs: 16 | opa-tests: 17 | name: OPA tests 18 | runs-on: ubuntu-20.04 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v2 22 | - name: Setup OPA 23 | run: | 24 | curl -L -o opa https://openpolicyagent.org/downloads/latest/opa_linux_amd64 25 | chmod 755 ./opa 26 | sudo mv ./opa /usr/local/bin 27 | - name: OPA Format 28 | run: | 29 | files=$(opa fmt --list .) 30 | if [ -n "$files" ]; then 31 | echo "=== The following files are not formatted ===" 32 | echo "$files" 33 | exit 1 34 | fi 35 | - name: OPA Test 36 | run: opa test -v kubernetes/ docker/ 37 | integration-tests: 38 | name: Integration tests 39 | runs-on: ubuntu-20.04 40 | steps: 41 | - name: Checkout code 42 | uses: actions/checkout@v2 43 | - name: Set up Go 44 | uses: actions/setup-go@v2 45 | with: 46 | go-version: ${{ env.GO_VERSION }} 47 | - name: Run integration tests 48 | run: go test -v ./... 49 | - name: Run metadata linter 50 | run: go run ./tools/lint 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .idea 3 | *.iml -------------------------------------------------------------------------------- /.manifest: -------------------------------------------------------------------------------- 1 | { 2 | "revision" : "[GITHUB_SHA]", 3 | "roots": ["docker", "kubernetes"] 4 | } 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: metadata_lint 2 | metadata_lint: 3 | go run ./tools/lint/ 4 | 5 | .PHONY: generate_missing_docs 6 | generate_missing_docs: 7 | go run ./tools/avd_generator -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This repository has moved 2 | 3 | Content has been migrated to https://github.com/aquasecurity/defsec. 4 | 5 | --- 6 | 7 | # Appshield 8 | 9 | This repository is a collection of policies for detecting mis-configurations, specifically security issues, in configuration files and Infrastructure as Code definitions. 10 | 11 | ## Contributing 12 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 13 | Please make sure to update tests as appropriate. 14 | 15 | ## Credits 16 | While building this repository, we were inspired by the following projects and would like to acknowledge their contribution to this repository: 17 | 18 | - https://github.com/hadolint/hadolint 19 | - https://github.com/Checkmarx/kics 20 | - https://github.com/controlplaneio/kubesec 21 | - https://github.com/aquasecurity/tfsec 22 | - https://kubernetes.io/docs/concepts/security/pod-security-standards/ 23 | - https://docs.docker.com/develop/develop-images/dockerfile_best-practices/ 24 | -------------------------------------------------------------------------------- /avd_docs/docker/AVD-DS-0001/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### ':latest' tag used 3 | When using a 'FROM' statement you should use a specific tag to avoid uncontrolled behavior when the image is updated. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | 12 | -------------------------------------------------------------------------------- /avd_docs/docker/AVD-DS-0002/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### root user 3 | Running containers with 'root' user can lead to a container escape situation. It is a best practice to run containers as non-root users, which can be done by adding a 'USER' statement to the Dockerfile. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://docs.docker.com/develop/develop-images/dockerfile_best-practices/ 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/docker/AVD-DS-0004/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### Port 22 exposed 3 | Exposing port 22 might allow users to SSH into the container. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | 12 | -------------------------------------------------------------------------------- /avd_docs/docker/AVD-DS-0005/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### ADD instead of COPY 3 | You should use COPY instead of ADD unless you want to extract a tar file. Note that an ADD command will extract a tar file, which adds the risk of Zip-based vulnerabilities. Accordingly, it is advised to use a COPY command, which does not extract tar files. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://docs.docker.com/engine/reference/builder/#add 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/docker/AVD-DS-0006/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### COPY '--from' referring to the current image 3 | COPY '--from' should not mention the current FROM alias, since it is impossible to copy from itself. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://docs.docker.com/develop/develop-images/multistage-build/ 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/docker/AVD-DS-0007/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### Multiple ENTRYPOINT instructions listed 3 | There can only be one ENTRYPOINT instruction in a Dockerfile. Only the last ENTRYPOINT instruction in the Dockerfile will have an effect. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://docs.docker.com/engine/reference/builder/#entrypoint 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/docker/AVD-DS-0008/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### Exposed port out of range 3 | UNIX ports outside the range 0-65535 are exposed. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://docs.docker.com/engine/reference/builder/#expose 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/docker/AVD-DS-0009/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### WORKDIR path not absolute 3 | For clarity and reliability, you should always use absolute paths for your WORKDIR. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#workdir 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/docker/AVD-DS-0010/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### RUN using 'sudo' 3 | Avoid using 'RUN' with 'sudo' commands, as it can lead to unpredictable behavior. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://docs.docker.com/engine/reference/builder/#run 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/docker/AVD-DS-0011/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### COPY with more than two arguments not ending with slash 3 | When a COPY command has more than two arguments, the last one should end with a slash. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://docs.docker.com/engine/reference/builder/#copy 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/docker/AVD-DS-0012/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### Duplicate aliases defined in different FROMs 3 | Different FROMs can't have the same alias defined. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://docs.docker.com/develop/develop-images/multistage-build/ 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/docker/AVD-DS-0013/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### 'RUN cd ...' to change directory 3 | Use WORKDIR instead of proliferating instructions like 'RUN cd … && do-something', which are hard to read, troubleshoot, and maintain. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#workdir 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/docker/AVD-DS-0014/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### RUN using 'wget' and 'curl' 3 | Avoid using both 'wget' and 'curl' since these tools have the same effect. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#run 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/docker/AVD-DS-0015/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### 'yum clean all' missing 3 | You should use 'yum clean all' after using a 'yum install' command to clean package cached data and reduce image size. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#run 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/docker/AVD-DS-0016/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### Multiple CMD instructions listed 3 | There can only be one CMD instruction in a Dockerfile. If you list more than one CMD then only the last CMD will take effect. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://docs.docker.com/engine/reference/builder/#cmd 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/docker/AVD-DS-0017/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### 'RUN update' instruction alone 3 | The instruction 'RUN update' should always be followed by ' install' in the same RUN statement. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#run 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/docker/AVD-DS-0018/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### 'COPY --from' refers to alias not defined previously 3 | COPY commands with the flag '--from' should mention a previously defined FROM alias. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://docs.docker.com/develop/develop-images/multistage-build/ 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/docker/AVD-DS-0019/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### 'dnf clean all' missing 3 | Cached package data should be cleaned after installation to reduce image size. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://docs.docker.com/develop/develop-images/dockerfile_best-practices/ 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/docker/AVD-DS-0020/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### 'zypper clean' missing 3 | The layer and image size should be reduced by deleting unneeded caches after running zypper. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#run 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/docker/AVD-DS-0021/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### 'apt-get' missing '-y' to avoid manual input 3 | 'apt-get' calls should use the flag '-y' to avoid manual user input. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://docs.docker.com/engine/reference/builder/#run 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/docker/AVD-DS-0022/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### Deprecated MAINTAINER used 3 | MAINTAINER has been deprecated since Docker 1.13.0. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://docs.docker.com/engine/deprecated/#maintainer-in-dockerfile 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/docker/AVD-DS-0023/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### Multiple HEALTHCHECK defined 3 | Providing more than one HEALTHCHECK instruction per stage is confusing and error-prone. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://docs.docker.com/engine/reference/builder/#healthcheck 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/docker/AVD-DS-0024/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### 'apt-get dist-upgrade' used 3 | 'apt-get dist-upgrade' upgrades a major version so it doesn't make more sense in Dockerfile. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | 12 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/advanced/AVD-KSV-0004/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### Unused capabilities should be dropped (drop any) 3 | Security best practices require containers to run with minimal required capabilities. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://kubesec.io/basics/containers-securitycontext-capabilities-drop-index-all/ 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/advanced/AVD-KSV-0007/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### hostAliases is set 3 | Managing /etc/hosts aliases can prevent the container engine from modifying the file after a pod’s containers have already been started. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | 12 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/advanced/AVD-KSV-0032/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### All container images must start with the *.azurecr.io domain 3 | Containers should only use images from trusted registries. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | 12 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/advanced/AVD-KSV-0033/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### All container images must start with a GCR domain 3 | Containers should only use images from trusted GCR registries. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | 12 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/advanced/AVD-KSV-0034/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### Container images from public registries used 3 | Container images must not start with an empty prefix or a defined public registry domain. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | 12 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/advanced/AVD-KSV-0035/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### All container images must start with an ECR domain 3 | Container images from non-ECR registries should be forbidden. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | 12 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/advanced/AVD-KSV-0036/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### Protecting Pod service account tokens 3 | ensure that Pod specifications disable the secret token being mounted by setting automountServiceAccountToken: false 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/#serviceaccount-admission-controller 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/advanced/AVD-KSV-0037/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### User Pods should not be placed in kube-system namespace 3 | ensure that User pods are not placed in kube-system namespace 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://kubernetes.io/docs/reference/setup-tools/kubeadm/implementation-details/ 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/advanced/AVD-KSV-0038/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### Selector usage in network policies 3 | ensure that network policies selectors are applied to pods or namespaces to restricted ingress and egress traffic within the pod network 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://kubernetes.io/docs/tasks/administer-cluster/declare-network-policy/ 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/general/AVD-KSV-0003/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### Default capabilities not dropped 3 | The container should drop all default capabilities and add only those that are needed for its execution. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://kubesec.io/basics/containers-securitycontext-capabilities-drop-index-all/ 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/general/AVD-KSV-0005/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### SYS_ADMIN capability added 3 | SYS_ADMIN gives the processes running inside the container privileges that are equivalent to root. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://kubesec.io/basics/containers-securitycontext-capabilities-add-index-sys-admin/ 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/general/AVD-KSV-0006/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### hostPath volume mounted with docker.sock 3 | Mounting docker.sock from the host can give the container full root access to the host. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://kubesec.io/basics/spec-volumes-hostpath-path-var-run-docker-sock/ 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/general/AVD-KSV-0011/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### CPU not limited 3 | Enforcing CPU limits prevents DoS via resource exhaustion. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://cloud.google.com/blog/products/containers-kubernetes/kubernetes-best-practices-resource-requests-and-limits 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/general/AVD-KSV-0013/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### Image tag ':latest' used 3 | It is best to avoid using the ':latest' image tag when deploying containers in production. Doing so makes it hard to track which version of the image is running, and hard to roll back the version. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://kubernetes.io/docs/concepts/configuration/overview/#container-images 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/general/AVD-KSV-0014/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### Root file system is not read-only 3 | An immutable root file system prevents applications from writing to their local disk. This can limit intrusions, as attackers will not be able to tamper with the file system or write foreign executables to disk. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://kubesec.io/basics/containers-securitycontext-readonlyrootfilesystem-true/ 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/general/AVD-KSV-0015/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### CPU requests not specified 3 | When containers have resource requests specified, the scheduler can make better decisions about which nodes to place pods on, and how to deal with resource contention. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://cloud.google.com/blog/products/containers-kubernetes/kubernetes-best-practices-resource-requests-and-limits 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/general/AVD-KSV-0016/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### Memory requests not specified 3 | When containers have memory requests specified, the scheduler can make better decisions about which nodes to place pods on, and how to deal with resource contention. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://kubesec.io/basics/containers-resources-limits-memory/ 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/general/AVD-KSV-0018/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### Memory not limited 3 | Enforcing memory limits prevents DoS via resource exhaustion. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://kubesec.io/basics/containers-resources-limits-memory/ 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/general/AVD-KSV-0020/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### Runs with low user ID 3 | Force the container to run with user ID > 10000 to avoid conflicts with the host’s user table. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://kubesec.io/basics/containers-securitycontext-runasuser/ 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/general/AVD-KSV-0021/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### Runs with low group ID 3 | Force the container to run with group ID > 10000 to avoid conflicts with the host’s user table. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://kubesec.io/basics/containers-securitycontext-runasuser/ 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/general/AVD-KSV-0102/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### Tiller Is Deployed 3 | Check if Helm Tiller component is deployed. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | 12 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/pss/baseline/AVD-KSV-0002/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### Default AppArmor profile not set 3 | A program inside the container can bypass AppArmor protection policies. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/pss/baseline/AVD-KSV-0008/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### Access to host IPC namespace 3 | Sharing the host’s IPC namespace allows container processes to communicate with processes on the host. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/pss/baseline/AVD-KSV-0009/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### Access to host network 3 | Sharing the host’s network namespace permits processes in the pod to communicate with processes bound to the host’s loopback adapter. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/pss/baseline/AVD-KSV-0010/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### Access to host PID 3 | Sharing the host’s PID namespace allows visibility on host processes, potentially leaking information such as environment variables and configuration. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/pss/baseline/AVD-KSV-0017/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### Privileged container 3 | Privileged containers share namespaces with the host system and do not offer any security. They should be used exclusively for system containers that require high privileges. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/pss/baseline/AVD-KSV-0022/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### Non-default capabilities added 3 | Adding NET_RAW or capabilities beyond the default set must be disallowed. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/pss/baseline/AVD-KSV-0023/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### hostPath volumes mounted 3 | HostPath volumes must be forbidden. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/pss/baseline/AVD-KSV-0024/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### Access to host ports 3 | HostPorts should be disallowed, or at minimum restricted to a known list. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/pss/baseline/AVD-KSV-0025/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### SELinux custom options set 3 | Setting a custom SELinux user or role option should be forbidden. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/pss/baseline/AVD-KSV-0026/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### Unsafe sysctl options set 3 | Sysctls can disable security mechanisms or affect all containers on a host, and should be disallowed except for an allowed 'safe' subset. A sysctl is considered safe if it is namespaced in the container or the Pod, and it is isolated from other Pods or processes on the same Node. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/pss/baseline/AVD-KSV-0027/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### Non-default /proc masks set 3 | The default /proc masks are set up to reduce attack surface, and should be required. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/pss/restricted/AVD-KSV-0001/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### Process can elevate its own privileges 3 | A program inside the container can elevate its own privileges and run as root, which might give the program control over the container and node. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/pss/restricted/AVD-KSV-0012/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### Runs as root user 3 | 'runAsNonRoot' forces the running image to run as a non-root user to ensure least privileges. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/pss/restricted/AVD-KSV-0028/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### Non-ephemeral volume types used 3 | In addition to restricting HostPath volumes, usage of non-ephemeral volume types should be limited to those defined through PersistentVolumes. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/pss/restricted/AVD-KSV-0029/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### A root primary or supplementary GID set 3 | Containers should be forbidden from running with a root primary or supplementary GID. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted 13 | 14 | -------------------------------------------------------------------------------- /avd_docs/kubernetes/pss/restricted/AVD-KSV-0030/docs.md: -------------------------------------------------------------------------------- 1 | 2 | ### Default Seccomp profile not set 3 | The RuntimeDefault seccomp profile must be required, or allow specific additional profiles. 4 | 5 | ### Impact 6 | 7 | 8 | 9 | {{ remediationActions }} 10 | 11 | ### Links 12 | - https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted 13 | 14 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | The Dockerfile rego policies can find the following issues: 2 | 3 | 1. Last USER in the file should not be root (but there needs to be at least one USER statement) 4 | 2. Tag the version of the FROM image explicitly (unless its scratch) 5 | 3. Avoid using "latest" in the FROM statement 6 | 4. Delete the apt-get lists after installing 7 | 8 | Reference: https://github.com/hadolint/hadolint 9 | -------------------------------------------------------------------------------- /docker/lib/docker.rego: -------------------------------------------------------------------------------- 1 | package lib.docker 2 | 3 | from[instruction] { 4 | instruction := input.stages[_][_] 5 | instruction.Cmd == "from" 6 | } 7 | 8 | add[instruction] { 9 | instruction := input.stages[_][_] 10 | instruction.Cmd == "add" 11 | } 12 | 13 | run[instruction] { 14 | instruction := input.stages[_][_] 15 | instruction.Cmd == "run" 16 | } 17 | 18 | copy[instruction] { 19 | instruction := input.stages[_][_] 20 | instruction.Cmd == "copy" 21 | } 22 | 23 | stage_copies[stage_name] = copies { 24 | stage := input.stages[stage_name] 25 | copies := [copy | copy := stage[_]; copy.Cmd == "copy"] 26 | } 27 | 28 | entrypoint[instruction] { 29 | instruction := input.stages[_][_] 30 | instruction.Cmd == "entrypoint" 31 | } 32 | 33 | stage_entrypoints[stage_name] = entrypoints { 34 | stage := input.stages[stage_name] 35 | entrypoints := [entrypoint | entrypoint := stage[_]; entrypoint.Cmd == "entrypoint"] 36 | } 37 | 38 | stage_cmd[stage_name] = cmds { 39 | stage := input.stages[stage_name] 40 | cmds := [cmd | cmd := stage[_]; cmd.Cmd == "cmd"] 41 | } 42 | 43 | stage_healthcheck[stage_name] = hlthchecks { 44 | stage := input.stages[stage_name] 45 | hlthchecks := [hlthcheck | hlthcheck := stage[_]; hlthcheck.Cmd == "healthcheck"] 46 | } 47 | 48 | stage_user[stage_name] = users { 49 | stage := input.stages[stage_name] 50 | users := [cmd | cmd := stage[_]; cmd.Cmd == "user"] 51 | } 52 | 53 | expose[instruction] { 54 | instruction := input.stages[_][_] 55 | instruction.Cmd == "expose" 56 | } 57 | 58 | user[instruction] { 59 | instruction := input.stages[_][_] 60 | instruction.Cmd == "user" 61 | } 62 | 63 | workdir[instruction] { 64 | instruction := input.stages[_][_] 65 | instruction.Cmd == "workdir" 66 | } 67 | -------------------------------------------------------------------------------- /docker/policies/README.md: -------------------------------------------------------------------------------- 1 | Collection of docker policies 2 | -------------------------------------------------------------------------------- /docker/policies/add_instead_of_copy.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS005 2 | 3 | import data.lib.docker 4 | 5 | __rego_metadata__ := { 6 | "id": "DS005", 7 | "avd_id": "AVD-DS-0005", 8 | "title": "ADD instead of COPY", 9 | "short_code": "use-copy-over-add", 10 | "version": "v1.0.0", 11 | "severity": "LOW", 12 | "type": "Dockerfile Security Check", 13 | "description": "You should use COPY instead of ADD unless you want to extract a tar file. Note that an ADD command will extract a tar file, which adds the risk of Zip-based vulnerabilities. Accordingly, it is advised to use a COPY command, which does not extract tar files.", 14 | "recommended_actions": "Use COPY instead of ADD", 15 | "url": "https://docs.docker.com/engine/reference/builder/#add", 16 | } 17 | 18 | __rego_input__ := { 19 | "combine": false, 20 | "selector": [{"type": "dockerfile"}], 21 | } 22 | 23 | get_add[args] { 24 | add := docker.add[_] 25 | args := concat(" ", add.Value) 26 | 27 | not contains(args, ".tar") 28 | } 29 | 30 | deny[res] { 31 | args := get_add[_] 32 | res := sprintf("Consider using 'COPY %s' command instead of 'ADD %s'", [args, args]) 33 | } 34 | -------------------------------------------------------------------------------- /docker/policies/add_instead_of_copy_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS005 2 | 3 | test_mixed_commands_denied { 4 | r := deny with input as {"stages": {"alpine:3.13": [ 5 | {"Cmd": "add", "Value": ["/target/resources.tar.gz", "resources.jar"]}, 6 | {"Cmd": "add", "Value": ["/target/app.jar", "app.jar"]}, 7 | ]}} 8 | 9 | count(r) == 1 10 | r[_] == "Consider using 'COPY /target/app.jar app.jar' command instead of 'ADD /target/app.jar app.jar'" 11 | } 12 | 13 | test_add_command_denied { 14 | r := deny with input as {"stages": {"alpine:3.13": [{ 15 | "Cmd": "add", 16 | "Value": ["/target/app.jar", "app.jar"], 17 | }]}} 18 | 19 | count(r) == 1 20 | r[_] == "Consider using 'COPY /target/app.jar app.jar' command instead of 'ADD /target/app.jar app.jar'" 21 | } 22 | 23 | test_run_allowed { 24 | r := deny with input as {"stages": {"alpine:3.13": [{"Cmd": "run", "Value": ["tar -xjf /temp/package.file.tar.gz"]}]}} 25 | 26 | count(r) == 0 27 | } 28 | 29 | test_copy_allowed { 30 | r := deny with input as {"stages": {"alpine:3.13": [{"Cmd": "copy", "Value": ["test.txt", "test2.txt"]}]}} 31 | 32 | count(r) == 0 33 | } 34 | 35 | test_add_tar_allowed { 36 | r := deny with input as {"stages": {"alpine:3.13": [{"Cmd": "add", "Value": ["/target/resources.tar.gz", "resources.tar.gz"]}]}} 37 | 38 | count(r) == 0 39 | } 40 | -------------------------------------------------------------------------------- /docker/policies/apt_get_missing_yes_flag_to_avoid_manual_input.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS021 2 | 3 | import data.lib.docker 4 | 5 | __rego_metadata__ := { 6 | "id": "DS021", 7 | "avd_id": "AVD-DS-0021", 8 | "title": "'apt-get' missing '-y' to avoid manual input", 9 | "short_code": "use-apt-auto-confirm", 10 | "version": "v1.0.0", 11 | "severity": "HIGH", 12 | "type": "Dockerfile Security Check", 13 | "description": "'apt-get' calls should use the flag '-y' to avoid manual user input.", 14 | "recommended_actions": "Add '-y' flag to 'apt-get'", 15 | "url": "https://docs.docker.com/engine/reference/builder/#run", 16 | } 17 | 18 | __rego_input__ := { 19 | "combine": false, 20 | "selector": [{"type": "dockerfile"}], 21 | } 22 | 23 | deny[res] { 24 | args := get_apt_get[_] 25 | res := sprintf("'-y' flag is missed: '%s'", [args]) 26 | } 27 | 28 | get_apt_get[arg] { 29 | run = docker.run[_] 30 | 31 | count(run.Value) == 1 32 | arg := run.Value[0] 33 | 34 | is_apt_get(arg) 35 | 36 | not includes_assume_yes(arg) 37 | } 38 | 39 | # checking json array 40 | get_apt_get[arg] { 41 | run = docker.run[_] 42 | 43 | count(run.Value) > 1 44 | 45 | arg := concat(" ", run.Value) 46 | 47 | is_apt_get(arg) 48 | 49 | not includes_assume_yes(arg) 50 | } 51 | 52 | is_apt_get(command) { 53 | regex.match("apt-get (-(-)?[a-zA-Z]+ *)*install(-(-)?[a-zA-Z]+ *)*", command) 54 | } 55 | 56 | short_flags := `(-([a-xzA-XZ])*y([a-xzA-XZ])*)` 57 | 58 | long_flags := `(--yes)|(--assume-yes)` 59 | 60 | optional_not_related_flags := `\s*(-(-)?[a-zA-Z]+\s*)*` 61 | 62 | combined_flags := sprintf(`%s(%s|%s)%s`, [optional_not_related_flags, short_flags, long_flags, optional_not_related_flags]) 63 | 64 | # flags before command 65 | includes_assume_yes(command) { 66 | install_regexp := sprintf(`apt-get%sinstall`, [combined_flags]) 67 | regex.match(install_regexp, command) 68 | } 69 | 70 | # flags behind command 71 | includes_assume_yes(command) { 72 | install_regexp := sprintf(`apt-get%sinstall%s`, [optional_not_related_flags, combined_flags]) 73 | regex.match(install_regexp, command) 74 | } 75 | -------------------------------------------------------------------------------- /docker/policies/copy_from_references_current_from_alias.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS006 2 | 3 | import data.lib.docker 4 | 5 | __rego_metadata__ := { 6 | "id": "DS006", 7 | "avd_id": "AVD-DS-0006", 8 | "title": "COPY '--from' referring to the current image", 9 | "short_code": "no-self-referencing-copy-from", 10 | "version": "v1.0.0", 11 | "severity": "CRITICAL", 12 | "type": "Dockerfile Security Check", 13 | "description": "COPY '--from' should not mention the current FROM alias, since it is impossible to copy from itself.", 14 | "recommended_actions": "Change the '--from' so that it will not refer to itself", 15 | "url": "https://docs.docker.com/develop/develop-images/multistage-build/", 16 | } 17 | 18 | __rego_input__ := { 19 | "combine": false, 20 | "selector": [{"type": "dockerfile"}], 21 | } 22 | 23 | get_alias_from_copy[args] { 24 | copies := docker.stage_copies[stage] 25 | 26 | flag := copies[_].Flags[_] 27 | contains(flag, "--from=") 28 | parts := split(flag, "=") 29 | 30 | is_alias_current_from_alias(stage, parts[1]) 31 | args := parts[1] 32 | } 33 | 34 | is_alias_current_from_alias(current_name, current_alias) = allow { 35 | current_name_lower := lower(current_name) 36 | current_alias_lower := lower(current_alias) 37 | 38 | #expecting stage name as "myimage:tag as dep" 39 | [_, alias] := regex.split(`\s+as\s+`, current_name_lower) 40 | 41 | alias == current_alias 42 | 43 | allow = true 44 | } 45 | 46 | deny[res] { 47 | args := get_alias_from_copy[_] 48 | res := sprintf("'COPY --from' should not mention current alias '%s' since it is impossible to copy from itself", [args]) 49 | } 50 | -------------------------------------------------------------------------------- /docker/policies/copy_from_without_from_alias_defined_previously.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS018 2 | 3 | import data.lib.docker 4 | 5 | __rego_metadata__ := { 6 | "id": "DS018", 7 | "avd_id": "AVD-DS-0018", 8 | "title": "'COPY --from' refers to alias not defined previously", 9 | "short_code": "no-orphan-from-alias", 10 | "version": "v1.0.0", 11 | "severity": "HIGH", 12 | "type": "Dockerfile Security Check", 13 | "description": "COPY commands with the flag '--from' should mention a previously defined FROM alias.", 14 | "recommended_actions": "Specify an alias defined previously", 15 | "url": "https://docs.docker.com/develop/develop-images/multistage-build/", 16 | } 17 | 18 | __rego_input__ := { 19 | "combine": false, 20 | "selector": [{"type": "dockerfile"}], 21 | } 22 | 23 | get_copy_arg[arg] { 24 | copy := docker.copy[_] 25 | 26 | arg := copy.Flags[_] 27 | 28 | contains(arg, "--from=") 29 | not regex.match("--from=\\d+", arg) 30 | 31 | aux_split := split(arg, "=") 32 | 33 | not alias_exists(aux_split[1], copy.Stage) 34 | } 35 | 36 | deny[res] { 37 | arg := get_copy_arg[_] 38 | res := sprintf("The alias '%s' is not defined in the previous stages", [arg]) 39 | } 40 | 41 | alias_exists(from_alias, max_stage_idx) { 42 | alias := get_alias(max_stage_idx)[_] 43 | from_alias == alias 44 | } 45 | 46 | get_alias(max_stage_idx) = res { 47 | res := {alias | 48 | name := get_aliased_name(max_stage_idx)[_] 49 | [_, alias] := regex.split(`\s+as\s+`, name) 50 | } 51 | } 52 | 53 | get_aliased_name(max_stage_idx) = res { 54 | res := {n | 55 | c := input.stages[name][_] 56 | c.Stage <= max_stage_idx # there is another rule that covers self reference 57 | name_lower = lower(name) 58 | contains(name_lower, " as ") 59 | n := name_lower 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /docker/policies/copy_with_more_than_two_arguments_not_ending_with_slash.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS011 2 | 3 | import data.lib.docker 4 | 5 | __rego_metadata__ := { 6 | "id": "DS011", 7 | "avd_id": "AVD-DS-0011", 8 | "title": "COPY with more than two arguments not ending with slash", 9 | "short_code": "use-slash-for-copy-args", 10 | "version": "v1.0.0", 11 | "severity": "CRITICAL", 12 | "type": "Dockerfile Security Check", 13 | "description": "When a COPY command has more than two arguments, the last one should end with a slash.", 14 | "recommended_actions": "Add slash to last COPY argument", 15 | "url": "https://docs.docker.com/engine/reference/builder/#copy", 16 | } 17 | 18 | __rego_input__ := { 19 | "combine": false, 20 | "selector": [{"type": "dockerfile"}], 21 | } 22 | 23 | get_copy_arg[arg] { 24 | copy := docker.copy[_] 25 | 26 | cnt := count(copy.Value) 27 | cnt > 2 28 | 29 | arg := copy.Value[cnt - 1] 30 | not endswith(arg, "/") 31 | } 32 | 33 | deny[res] { 34 | arg := get_copy_arg[_] 35 | res := sprintf("Slash is expected at the end of COPY command argument '%s'", [arg]) 36 | } 37 | -------------------------------------------------------------------------------- /docker/policies/copy_with_more_than_two_arguments_not_ending_with_slash_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS011 2 | 3 | test_basic_denied { 4 | r := deny with input as {"stages": {"alpine:3.3": [ 5 | { 6 | "Cmd": "from", 7 | "Value": ["node:carbon2"], 8 | }, 9 | { 10 | "Cmd": "copy", 11 | "Value": ["package.json", "yarn.lock", "my_app"], 12 | }, 13 | ]}} 14 | 15 | count(r) == 1 16 | r[_] == "Slash is expected at the end of COPY command argument 'my_app'" 17 | } 18 | 19 | test_two_args_allowed { 20 | r := deny with input as {"stages": {"alpine:3.3": [ 21 | { 22 | "Cmd": "from", 23 | "Value": ["node:carbon2"], 24 | }, 25 | { 26 | "Cmd": "copy", 27 | "Value": ["package.json", "yarn.lock"], 28 | }, 29 | ]}} 30 | 31 | count(r) == 0 32 | } 33 | 34 | test_three_arg_allowed { 35 | r := deny with input as {"stages": {"alpine:3.3": [ 36 | { 37 | "Cmd": "from", 38 | "Value": ["node:carbon2"], 39 | }, 40 | { 41 | "Cmd": "copy", 42 | "Value": ["package.json", "yarn.lock", "myapp/"], 43 | }, 44 | ]}} 45 | 46 | count(r) == 0 47 | } 48 | -------------------------------------------------------------------------------- /docker/policies/index.yaml: -------------------------------------------------------------------------------- 1 | - id: DS001 2 | file: latest_tag.rego 3 | - id: DS002 4 | file: root_user.rego 5 | - id: DS004 6 | file: port22.rego 7 | - id: DS005 8 | file: add_instead_of_copy.rego 9 | - id: DS006 10 | file: copy_from_references_current_from_alias.rego 11 | - id: DS007 12 | file: multiple_entrypoint_instructions_listed.rego 13 | - id: DS008 14 | file: unix_ports_out_of_range.rego 15 | - id: DS009 16 | file: workdir_path_not_absolute.rego 17 | - id: DS010 18 | file: run_using_sudo.rego 19 | - id: DS011 20 | file: copy_with_more_than_two_arguments_not_ending_with_slash.rego 21 | - id: DS012 22 | file: same_alias_in_different_froms.rego 23 | - id: DS013 24 | file: run_command_cd_instead_of_workdir.rego 25 | - id: DS014 26 | file: run_using_wget_and_curl.rego 27 | - id: DS015 28 | file: yum_clean_all_missing.rego 29 | - id: DS016 30 | file: multiple_cmd_instructions_listed.rego 31 | - id: DS017 32 | file: update_instruction_alone.rego 33 | - id: DS018 34 | file: copy_from_without_from_alias_defined_previously.rego 35 | - id: DS019 36 | file: missing_dnf_clean_all.rego 37 | - id: DS020 38 | file: missing_zypper_clean.rego 39 | - id: DS021 40 | file: apt_get_missing_yes_flag_to_avoid_manual_input.rego 41 | - id: DS022 42 | file: maintainer_is_deprecated.rego 43 | - id: DS023 44 | file: multiple_healthcheck_instructions.rego 45 | - id: DS024 46 | file: run_apt_get_dist_upgrade.rego 47 | -------------------------------------------------------------------------------- /docker/policies/latest_tag.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS001 2 | 3 | import data.lib.docker 4 | 5 | __rego_metadata__ := { 6 | "id": "DS001", 7 | "avd_id": "AVD-DS-0001", 8 | "title": "':latest' tag used", 9 | "short_code": "use-specific-tags", 10 | "version": "v1.0.0", 11 | "severity": "MEDIUM", 12 | "type": "Dockerfile Security Check", 13 | "description": "When using a 'FROM' statement you should use a specific tag to avoid uncontrolled behavior when the image is updated.", 14 | "recommended_actions": "Add a tag to the image in the 'FROM' statement", 15 | } 16 | 17 | __rego_input__ := { 18 | "combine": false, 19 | "selector": [{"type": "dockerfile"}], 20 | } 21 | 22 | # returns element after AS 23 | get_alias(values) = alias { 24 | "as" == lower(values[i]) 25 | alias = values[i + 1] 26 | } 27 | 28 | get_aliases[aliases] { 29 | from_cmd := docker.from[_] 30 | aliases := get_alias(from_cmd.Value) 31 | } 32 | 33 | is_alias(img) { 34 | img == get_aliases[_] 35 | } 36 | 37 | # image_names returns the image in FROM statement. 38 | image_names[image_name] { 39 | from := docker.from[_] 40 | image_name := from.Value[0] 41 | } 42 | 43 | # image_tags returns the image and tag. 44 | parse_tag(name) = [img, tag] { 45 | [img, tag] = split(name, ":") 46 | } 47 | 48 | # image_tags returns the image and "latest" if a tag is not specified. 49 | parse_tag(img) = [img, tag] { 50 | tag := "latest" 51 | not contains(img, ":") 52 | } 53 | 54 | #base scenario 55 | image_tags[[img, tag]] { 56 | name := image_names[_] 57 | not startswith(name, "$") 58 | [img, tag] = parse_tag(name) 59 | } 60 | 61 | #If variable is using with FROM then it's value should contain a tag 62 | image_tags[[img, tag]] { 63 | some i, j, k, l 64 | name := image_names[i] 65 | 66 | cmd_obj := input.stages[j][k] 67 | 68 | possibilities := {"arg", "env"} 69 | cmd_obj.Cmd == possibilities[l] 70 | 71 | bare_var := trim_prefix(name, "$") 72 | 73 | startswith(cmd_obj.Value[0], bare_var) 74 | 75 | [_, bare_image_name] := regex.split(`\s*=\s*`, cmd_obj.Value[0]) 76 | 77 | [img, tag] = parse_tag(bare_image_name) 78 | } 79 | 80 | # fail_latest is true if image is not scratch 81 | # and image is not an alias 82 | # and tag is latest. 83 | fail_latest[img] { 84 | [img, tag] := image_tags[_] 85 | img != "scratch" 86 | not is_alias(img) 87 | tag == "latest" 88 | } 89 | 90 | deny[res] { 91 | img := fail_latest[_] 92 | res := sprintf("Specify a tag in the 'FROM' statement for image '%s'", [img]) 93 | } 94 | -------------------------------------------------------------------------------- /docker/policies/maintainer_is_deprecated.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS022 2 | 3 | import data.lib.docker 4 | 5 | __rego_metadata__ := { 6 | "id": "DS022", 7 | "avd_id": "AVD-DS-0022", 8 | "title": "Deprecated MAINTAINER used", 9 | "short_code": "no-maintainer", 10 | "version": "v1.0.0", 11 | "severity": "HIGH", 12 | "type": "Dockerfile Security Check", 13 | "description": "MAINTAINER has been deprecated since Docker 1.13.0.", 14 | "recommended_actions": "Use LABEL instead of MAINTAINER", 15 | "url": "https://docs.docker.com/engine/deprecated/#maintainer-in-dockerfile", 16 | } 17 | 18 | __rego_input__ := { 19 | "combine": false, 20 | "selector": [{"type": "dockerfile"}], 21 | } 22 | 23 | get_maintainer[mntnr] { 24 | mntnr := input.stages[_][_] 25 | mntnr.Cmd == "maintainer" 26 | } 27 | 28 | deny[res] { 29 | mntnr := get_maintainer[_] 30 | res := sprintf("MAINTAINER should not be used: 'MAINTAINER %s'", [mntnr.Value[0]]) 31 | } 32 | -------------------------------------------------------------------------------- /docker/policies/maintainer_is_deprecated_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS022 2 | 3 | test_denied { 4 | r := deny with input as {"stages": {"fedora:27": [ 5 | { 6 | "Cmd": "from", 7 | "Value": ["fedora:27"], 8 | }, 9 | { 10 | "Cmd": "maintainer", 11 | "Value": ["admin@example.com"], 12 | }, 13 | ]}} 14 | 15 | count(r) == 1 16 | r[_] == "MAINTAINER should not be used: 'MAINTAINER admin@example.com'" 17 | } 18 | 19 | test_allowed { 20 | r := deny with input as {"stages": {"fedora:27": [{ 21 | "Cmd": "from", 22 | "Value": ["fedora:27"], 23 | }]}} 24 | 25 | count(r) == 0 26 | } 27 | -------------------------------------------------------------------------------- /docker/policies/missing_dnf_clean_all.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS019 2 | 3 | import data.lib.docker 4 | 5 | __rego_metadata__ := { 6 | "id": "DS019", 7 | "avd_id": "AVD-DS-0019", 8 | "title": "'dnf clean all' missing", 9 | "short_code": "purge-dnf-package-cache", 10 | "version": "v1.0.0", 11 | "severity": "HIGH", 12 | "type": "Dockerfile Security Check", 13 | "description": "Cached package data should be cleaned after installation to reduce image size.", 14 | "recommended_actions": "Add 'dnf clean all' to Dockerfile", 15 | "url": "https://docs.docker.com/develop/develop-images/dockerfile_best-practices/", 16 | } 17 | 18 | __rego_input__ := { 19 | "combine": false, 20 | "selector": [{"type": "dockerfile"}], 21 | } 22 | 23 | install_regex := `(dnf install)|(dnf in)|(dnf reinstall)|(dnf rei)|(dnf install-n)|(dnf install-na)|(dnf install-nevra)` 24 | 25 | dnf_regex = sprintf("%s|(dnf clean all)", [install_regex]) 26 | 27 | get_dnf[arg] { 28 | run := docker.run[_] 29 | arg := run.Value[0] 30 | 31 | regex.match(install_regex, arg) 32 | 33 | not contains_clean_after_dnf(arg) 34 | } 35 | 36 | deny[res] { 37 | args := get_dnf[_] 38 | res := sprintf("'dnf clean all' is missed: %s", [args]) 39 | } 40 | 41 | contains_clean_after_dnf(cmd) { 42 | dnf_commands := regex.find_n(dnf_regex, cmd, -1) 43 | 44 | dnf_commands[count(dnf_commands) - 1] == "dnf clean all" 45 | } 46 | -------------------------------------------------------------------------------- /docker/policies/missing_zypper_clean.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS020 2 | 3 | import data.lib.docker 4 | 5 | __rego_metadata__ := { 6 | "id": "DS020", 7 | "avd_id": "AVD-DS-0020", 8 | "title": "'zypper clean' missing", 9 | "short_code": "purge-zipper-cache", 10 | "version": "v1.0.0", 11 | "severity": "HIGH", 12 | "type": "Dockerfile Security Check", 13 | "description": "The layer and image size should be reduced by deleting unneeded caches after running zypper.", 14 | "recommended_actions": "Add 'zypper clean' to Dockerfile", 15 | "url": "https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#run", 16 | } 17 | 18 | __rego_input__ := { 19 | "combine": false, 20 | "selector": [{"type": "dockerfile"}], 21 | } 22 | 23 | install_regex := `(zypper in)|(zypper remove)|(zypper rm)|(zypper source-install)|(zypper si)|(zypper patch)|(zypper (-(-)?[a-zA-Z]+ *)*install)` 24 | 25 | zypper_regex = sprintf("%s|(zypper clean)|(zypper cc)", [install_regex]) 26 | 27 | get_zypper[arg] { 28 | run := docker.run[_] 29 | arg := run.Value[0] 30 | 31 | regex.match(install_regex, arg) 32 | 33 | not contains_zipper_clean(arg) 34 | } 35 | 36 | deny[res] { 37 | args := get_zypper[_] 38 | res := sprintf("'zypper clean' is missed: '%s'", [args]) 39 | } 40 | 41 | contains_zipper_clean(cmd) { 42 | zypper_commands := regex.find_n(zypper_regex, cmd, -1) 43 | 44 | is_zypper_clean(zypper_commands[count(zypper_commands) - 1]) 45 | } 46 | 47 | is_zypper_clean(cmd) { 48 | cmd == "zypper clean" 49 | } 50 | 51 | is_zypper_clean(cmd) { 52 | cmd == "zypper cc" 53 | } 54 | -------------------------------------------------------------------------------- /docker/policies/multiple_cmd_instructions_listed.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS016 2 | 3 | import data.lib.docker 4 | 5 | __rego_metadata__ := { 6 | "id": "DS016", 7 | "avd_id": "AVD-DS-0016", 8 | "title": "Multiple CMD instructions listed", 9 | "short_code": "only-one-cmd", 10 | "version": "v1.0.0", 11 | "severity": "HIGH", 12 | "type": "Dockerfile Security Check", 13 | "description": "There can only be one CMD instruction in a Dockerfile. If you list more than one CMD then only the last CMD will take effect.", 14 | "recommended_actions": "Dockefile should only have one CMD instruction. Remove all the other CMD instructions", 15 | "url": "https://docs.docker.com/engine/reference/builder/#cmd", 16 | } 17 | 18 | __rego_input__ := { 19 | "combine": false, 20 | "selector": [{"type": "dockerfile"}], 21 | } 22 | 23 | deny[res] { 24 | cmds := docker.stage_cmd[name] 25 | cnt := count(cmds) 26 | cnt > 1 27 | res := sprintf("There are %d duplicate CMD instructions for stage '%s'", [cnt, name]) 28 | } 29 | -------------------------------------------------------------------------------- /docker/policies/multiple_cmd_instructions_listed_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS016 2 | 3 | test_denied { 4 | r := deny with input as {"stages": { 5 | "golang:1.7.3": [ 6 | { 7 | "Cmd": "from", 8 | "Value": ["golang:1.7.3"], 9 | }, 10 | { 11 | "Cmd": "cmd", 12 | "Value": ["./app"], 13 | }, 14 | { 15 | "Cmd": "cmd", 16 | "Value": ["./apps"], 17 | }, 18 | ], 19 | "alpine:latest": [ 20 | { 21 | "Cmd": "from", 22 | "Value": ["alpine:latest"], 23 | }, 24 | { 25 | "Cmd": "cmd", 26 | "Value": ["./app"], 27 | }, 28 | ], 29 | }} 30 | 31 | count(r) == 1 32 | r[_] == "There are 2 duplicate CMD instructions for stage 'golang:1.7.3'" 33 | } 34 | 35 | test_allowed { 36 | r := deny with input as {"stages": { 37 | "golang:1.7.3": [ 38 | { 39 | "Cmd": "from", 40 | "Value": ["golang:1.7.3"], 41 | }, 42 | { 43 | "Cmd": "cmd", 44 | "Value": ["./app"], 45 | }, 46 | ], 47 | "alpine:latest": [ 48 | { 49 | "Cmd": "from", 50 | "Value": ["alpine:latest"], 51 | }, 52 | { 53 | "Cmd": "cmd", 54 | "Value": ["./app"], 55 | }, 56 | ], 57 | }} 58 | 59 | count(r) == 0 60 | } 61 | -------------------------------------------------------------------------------- /docker/policies/multiple_entrypoint_instructions_listed.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS007 2 | 3 | import data.lib.docker 4 | 5 | __rego_metadata__ := { 6 | "id": "DS007", 7 | "avd_id": "AVD-DS-0007", 8 | "title": "Multiple ENTRYPOINT instructions listed", 9 | "short_code": "only-one-entrypoint", 10 | "version": "v1.0.0", 11 | "severity": "CRITICAL", 12 | "type": "Dockerfile Security Check", 13 | "description": "There can only be one ENTRYPOINT instruction in a Dockerfile. Only the last ENTRYPOINT instruction in the Dockerfile will have an effect.", 14 | "recommended_actions": "Remove unnecessary ENTRYPOINT instruction.", 15 | "url": "https://docs.docker.com/engine/reference/builder/#entrypoint", 16 | } 17 | 18 | __rego_input__ := { 19 | "combine": false, 20 | "selector": [{"type": "dockerfile"}], 21 | } 22 | 23 | deny[res] { 24 | entrypoints := docker.stage_entrypoints[name] 25 | count(entrypoints) > 1 26 | res := sprintf("There are %d duplicate ENTRYPOINT instructions for stage '%s'", [count(entrypoints), name]) 27 | } 28 | -------------------------------------------------------------------------------- /docker/policies/multiple_entrypoint_instructions_listed_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS007 2 | 3 | test_denied { 4 | r := deny with input as {"stages": { 5 | "golang": [ 6 | { 7 | "Cmd": "from", 8 | "Value": ["golang:1.7.3"], 9 | }, 10 | { 11 | "Cmd": "entrypoint", 12 | "Value": [ 13 | "/opt/app/run.sh", 14 | "--port", 15 | "8080", 16 | ], 17 | }, 18 | { 19 | "Cmd": "entrypoint", 20 | "Value": [ 21 | "/opt/app/run.sh", 22 | "--port", 23 | "8000", 24 | ], 25 | }, 26 | ], 27 | "alpine": [ 28 | { 29 | "Cmd": "from", 30 | "Value": ["alpine:latest"], 31 | }, 32 | { 33 | "Cmd": "entrypoint", 34 | "Value": [ 35 | "/opt/app/run.sh", 36 | "--port", 37 | "8080", 38 | ], 39 | }, 40 | ], 41 | }} 42 | 43 | count(r) == 1 44 | r[_] == "There are 2 duplicate ENTRYPOINT instructions for stage 'golang'" 45 | } 46 | 47 | test_allowed { 48 | r := deny with input as {"stages": { 49 | "golang": [ 50 | { 51 | "Cmd": "from", 52 | "Value": ["golang:1.7.3"], 53 | }, 54 | { 55 | "Cmd": "entrypoint", 56 | "Value": [ 57 | "/opt/app/run.sh", 58 | "--port", 59 | "8080", 60 | ], 61 | }, 62 | ], 63 | "alpine": [ 64 | { 65 | "Cmd": "from", 66 | "Value": ["alpine:latest"], 67 | }, 68 | { 69 | "Cmd": "entrypoint", 70 | "Value": [ 71 | "/opt/app/run.sh", 72 | "--port", 73 | "8080", 74 | ], 75 | }, 76 | ], 77 | }} 78 | 79 | count(r) == 0 80 | } 81 | -------------------------------------------------------------------------------- /docker/policies/multiple_healthcheck_instructions.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS023 2 | 3 | import data.lib.docker 4 | 5 | __rego_metadata__ := { 6 | "id": "DS023", 7 | "avd_id": "AVD-DS-0023", 8 | "title": "Multiple HEALTHCHECK defined", 9 | "short_code": "only-one-healthcheck", 10 | "version": "v1.0.0", 11 | "severity": "MEDIUM", 12 | "type": "Dockerfile Security Check", 13 | "description": "Providing more than one HEALTHCHECK instruction per stage is confusing and error-prone.", 14 | "recommended_actions": "One HEALTHCHECK instruction must remain in Dockerfile. Remove all other instructions.", 15 | "url": "https://docs.docker.com/engine/reference/builder/#healthcheck", 16 | } 17 | 18 | __rego_input__ := { 19 | "combine": false, 20 | "selector": [{"type": "dockerfile"}], 21 | } 22 | 23 | deny[res] { 24 | healthchecks := docker.stage_healthcheck[name] 25 | cnt := count(healthchecks) 26 | cnt > 1 27 | res := sprintf("There are %d duplicate HEALTHCHECK instructions in the stage '%s'", [cnt, name]) 28 | } 29 | -------------------------------------------------------------------------------- /docker/policies/multiple_healthcheck_instructions_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS023 2 | 3 | test_denied { 4 | r := deny with input as {"stages": { 5 | "golang:1.7.3": [ 6 | { 7 | "Cmd": "from", 8 | "Value": ["busybox"], 9 | }, 10 | { 11 | "Cmd": "healthcheck", 12 | "Value": [ 13 | "CMD", 14 | "curl http://localhost:8080", 15 | ], 16 | }, 17 | { 18 | "Cmd": "healthcheck", 19 | "Value": [ 20 | "CMD", 21 | "/bin/healthcheck", 22 | ], 23 | }, 24 | ], 25 | "alpine:latest": [ 26 | { 27 | "Cmd": "from", 28 | "Value": ["alpine:latest"], 29 | }, 30 | { 31 | "Cmd": "healthcheck", 32 | "Value": [ 33 | "CMD", 34 | "/bin/healthcheck", 35 | ], 36 | }, 37 | { 38 | "Cmd": "cmd", 39 | "Value": ["./app"], 40 | }, 41 | ], 42 | }} 43 | 44 | count(r) == 1 45 | r[_] == "There are 2 duplicate HEALTHCHECK instructions in the stage 'golang:1.7.3'" 46 | } 47 | 48 | test_allowed { 49 | r := deny with input as {"stages": { 50 | "golang:1.7.3": [ 51 | { 52 | "Cmd": "from", 53 | "Value": ["golang:1.7.3"], 54 | }, 55 | { 56 | "Cmd": "healthcheck", 57 | "Value": [ 58 | "CMD", 59 | "/bin/healthcheck", 60 | ], 61 | }, 62 | { 63 | "Cmd": "cmd", 64 | "Value": ["./app"], 65 | }, 66 | ], 67 | "alpine:latest": [ 68 | { 69 | "Cmd": "from", 70 | "Value": ["alpine:latest"], 71 | }, 72 | { 73 | "Cmd": "cmd", 74 | "Value": ["./app"], 75 | }, 76 | ], 77 | }} 78 | 79 | count(r) == 0 80 | } 81 | 82 | test_healthcheck_none_allowed { 83 | r := deny with input as {"stages": { 84 | "golang:1.7.3": [ 85 | { 86 | "Cmd": "from", 87 | "Value": ["golang:1.7.3"], 88 | }, 89 | { 90 | "Cmd": "healthcheck", 91 | "Value": ["NONE"], 92 | }, 93 | { 94 | "Cmd": "cmd", 95 | "Value": ["./app"], 96 | }, 97 | ], 98 | "alpine:latest": [ 99 | { 100 | "Cmd": "from", 101 | "Value": ["alpine:latest"], 102 | }, 103 | { 104 | "Cmd": "cmd", 105 | "Value": ["./app"], 106 | }, 107 | ], 108 | }} 109 | 110 | count(r) == 0 111 | } 112 | -------------------------------------------------------------------------------- /docker/policies/port22.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS004 2 | 3 | import data.lib.docker 4 | 5 | __rego_metadata__ := { 6 | "id": "DS004", 7 | "avd_id": "AVD-DS-0004", 8 | "title": "Port 22 exposed", 9 | "short_code": "no-ssh-port", 10 | "version": "v1.0.0", 11 | "severity": "MEDIUM", 12 | "type": "Dockerfile Security Check", 13 | "description": "Exposing port 22 might allow users to SSH into the container.", 14 | "recommended_actions": "Remove 'EXPOSE 22' statement from the Dockerfile", 15 | } 16 | 17 | __rego_input__ := { 18 | "combine": false, 19 | "selector": [{"type": "dockerfile"}], 20 | } 21 | 22 | # deny_list contains the port numbers which needs to be denied. 23 | denied_ports := ["22", "22/tcp", "22/udp"] 24 | 25 | # fail_port_check is true if the Dockerfile contains an expose statement for value 22 26 | fail_port_check { 27 | expose := docker.expose[_] 28 | expose.Value[_] == denied_ports[_] 29 | } 30 | 31 | deny[res] { 32 | fail_port_check 33 | res := "Port 22 should not be exposed in Dockerfile" 34 | } 35 | -------------------------------------------------------------------------------- /docker/policies/port22_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS004 2 | 3 | # Test EXPOSE with PORT 22 4 | test_denied { 5 | r := deny with input as {"stages": {"alpine:3.13": [{ 6 | "Cmd": "expose", 7 | "Value": ["22"], 8 | }]}} 9 | 10 | count(r) > 0 11 | startswith(r[_], "Port 22 should not be exposed in Dockerfile") 12 | } 13 | 14 | test_tcp_denied { 15 | r := deny with input as {"stages": {"alpine:3.13": [{ 16 | "Cmd": "expose", 17 | "Value": ["22/tcp"], 18 | }]}} 19 | 20 | count(r) > 0 21 | startswith(r[_], "Port 22 should not be exposed in Dockerfile") 22 | } 23 | 24 | # Test EXPOSE without PORT 22 25 | test_allowed { 26 | r := deny with input as {"stages": {"alpine:3.13": [{ 27 | "Cmd": "expose", 28 | "Value": ["8080"], 29 | }]}} 30 | 31 | count(r) == 0 32 | } 33 | -------------------------------------------------------------------------------- /docker/policies/root_user.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS002 2 | 3 | import data.lib.docker 4 | 5 | __rego_metadata__ := { 6 | "id": "DS002", 7 | "avd_id": "AVD-DS-0002", 8 | "title": "root user", 9 | "short_code": "least-privilege-user", 10 | "version": "v1.0.0", 11 | "severity": "HIGH", 12 | "type": "Dockerfile Security Check", 13 | "description": "Running containers with 'root' user can lead to a container escape situation. It is a best practice to run containers as non-root users, which can be done by adding a 'USER' statement to the Dockerfile.", 14 | "recommended_actions": "Add 'USER ' line to the Dockerfile", 15 | "url": "https://docs.docker.com/develop/develop-images/dockerfile_best-practices/", 16 | } 17 | 18 | __rego_input__ := { 19 | "combine": false, 20 | "selector": [{"type": "dockerfile"}], 21 | } 22 | 23 | # get_user returns all the usernames from 24 | # the USER command. 25 | get_user[username] { 26 | user := docker.user[_] 27 | username := user.Value[_] 28 | } 29 | 30 | # fail_user_count is true if there is no USER command. 31 | fail_user_count { 32 | count(get_user) < 1 33 | } 34 | 35 | # fail_last_user_root is true if the last USER command 36 | # value is "root" 37 | fail_last_user_root { 38 | stage_users := docker.stage_user[_] 39 | len := count(stage_users) 40 | stage_users[len - 1].Value[0] == "root" 41 | } 42 | 43 | deny[msg] { 44 | fail_user_count 45 | msg = "Specify at least 1 USER command in Dockerfile with non-root user as argument" 46 | } 47 | 48 | deny[res] { 49 | fail_last_user_root 50 | res := "Last USER command in Dockerfile should not be 'root'" 51 | } 52 | -------------------------------------------------------------------------------- /docker/policies/root_user_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS002 2 | 3 | import data.lib.docker 4 | 5 | test_not_root_allowed { 6 | r := deny with input as {"stages": {"alpine:3.13": [{ 7 | "Cmd": "user", 8 | "Value": ["user1", "user2"], 9 | }]}} 10 | 11 | count(r) == 0 12 | } 13 | 14 | test_last_non_root_allowed { 15 | r := deny with input as {"stages": {"alpine:3.13": [ 16 | { 17 | "Cmd": "user", 18 | "Value": ["root"], 19 | }, 20 | { 21 | "Cmd": "user", 22 | "Value": ["user1"], 23 | }, 24 | ]}} 25 | 26 | count(r) == 0 27 | } 28 | 29 | test_no_user_cmd_denied { 30 | r := deny with input as {"stages": {"alpine:3.13": [{ 31 | "Cmd": "expose", 32 | "Value": [22], 33 | }]}} 34 | 35 | count(r) == 1 36 | startswith(r[_], "Specify at least 1 USER command in Dockerfile") 37 | } 38 | 39 | test_last_root_denied { 40 | r := deny with input as {"stages": {"alpine:3.13": [ 41 | { 42 | "Cmd": "run", 43 | "Value": ["apt-get update"], 44 | }, 45 | { 46 | "Cmd": "user", 47 | "Value": ["user1"], 48 | }, 49 | { 50 | "Cmd": "run", 51 | "Value": ["apt-get update"], 52 | }, 53 | { 54 | "Cmd": "user", 55 | "Value": ["root"], 56 | }, 57 | { 58 | "Cmd": "run", 59 | "Value": ["apt-get update"], 60 | }, 61 | ]}} 62 | 63 | count(r) > 0 64 | startswith(r[_], "Last USER command in Dockerfile should not be 'root'") 65 | } 66 | 67 | test_last_root_case_2 { 68 | r := deny with input as {"stages": {"alpine:3.13": [ 69 | { 70 | "Cmd": "user", 71 | "Value": ["user1"], 72 | }, 73 | { 74 | "Cmd": "user", 75 | "Value": ["root"], 76 | }, 77 | ]}} 78 | 79 | count(r) > 0 80 | startswith(r[_], "Last USER command in Dockerfile should not be 'root'") 81 | } 82 | 83 | test_empty_user_denied { 84 | r := deny with input as {"stages": {"alpine:3.13": [{ 85 | "Cmd": "user", 86 | "Value": [], 87 | }]}} 88 | 89 | count(r) == 1 90 | startswith(r[_], "Specify at least 1 USER command in Dockerfile") 91 | } 92 | -------------------------------------------------------------------------------- /docker/policies/run_apt_get_dist_upgrade.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS024 2 | 3 | import data.lib.docker 4 | 5 | __rego_metadata__ := { 6 | "id": "DS024", 7 | "avd_id": "AVD-DS-0024", 8 | "title": "'apt-get dist-upgrade' used", 9 | "short_code": "no-dist-upgrade", 10 | "version": "v1.0.0", 11 | "severity": "HIGH", 12 | "type": "Dockerfile Security Check", 13 | "description": "'apt-get dist-upgrade' upgrades a major version so it doesn't make more sense in Dockerfile.", 14 | "recommended_actions": "Just use different image", 15 | } 16 | 17 | __rego_input__ := { 18 | "combine": false, 19 | "selector": [{"type": "dockerfile"}], 20 | } 21 | 22 | get_apt_get_dist_upgrade[args] { 23 | run := docker.run[_] 24 | regex.match(`apt-get .* dist-upgrade`, run.Value[0]) 25 | args := run.Value[0] 26 | } 27 | 28 | deny[res] { 29 | get_apt_get_dist_upgrade[_] 30 | res := "'apt-get dist-upgrade' should not be used in Dockerfile" 31 | } 32 | -------------------------------------------------------------------------------- /docker/policies/run_apt_get_dist_upgrade_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS024 2 | 3 | test_denied { 4 | r := deny with input as {"stages": {"debian": [ 5 | { 6 | "Cmd": "from", 7 | "Value": ["debian"], 8 | }, 9 | { 10 | "Cmd": "run", 11 | "Value": ["apt-get update && apt-get dist-upgrade"], 12 | }, 13 | { 14 | "Cmd": "cmd", 15 | "Value": [ 16 | "python", 17 | "/usr/src/app/app.py", 18 | ], 19 | }, 20 | ]}} 21 | 22 | count(r) == 1 23 | r[_] == "'apt-get dist-upgrade' should not be used in Dockerfile" 24 | } 25 | 26 | test_shortflag_denied { 27 | r := deny with input as {"stages": {"debian": [ 28 | { 29 | "Cmd": "from", 30 | "Value": ["debian"], 31 | }, 32 | { 33 | "Cmd": "run", 34 | "Value": ["apt-get update && apt-get -q dist-upgrade"], 35 | }, 36 | { 37 | "Cmd": "cmd", 38 | "Value": [ 39 | "python", 40 | "/usr/src/app/app.py", 41 | ], 42 | }, 43 | ]}} 44 | 45 | count(r) == 1 46 | r[_] == "'apt-get dist-upgrade' should not be used in Dockerfile" 47 | } 48 | 49 | test_longflag_denied { 50 | r := deny with input as {"stages": {"debian": [ 51 | { 52 | "Cmd": "from", 53 | "Value": ["debian"], 54 | }, 55 | { 56 | "Cmd": "run", 57 | "Value": ["apt-get update && apt-get --quiet dist-upgrade"], 58 | }, 59 | { 60 | "Cmd": "cmd", 61 | "Value": [ 62 | "python", 63 | "/usr/src/app/app.py", 64 | ], 65 | }, 66 | ]}} 67 | 68 | count(r) == 1 69 | r[_] == "'apt-get dist-upgrade' should not be used in Dockerfile" 70 | } 71 | 72 | test_allowed { 73 | r := deny with input as {"stages": {"debian": [ 74 | { 75 | "Cmd": "from", 76 | "Value": ["debian"], 77 | }, 78 | { 79 | "Cmd": "run", 80 | "Value": ["apt-get update && apt-get upgrade"], 81 | }, 82 | { 83 | "Cmd": "cmd", 84 | "Value": [ 85 | "python", 86 | "/usr/src/app/app.py", 87 | ], 88 | }, 89 | ]}} 90 | 91 | count(r) == 0 92 | } 93 | -------------------------------------------------------------------------------- /docker/policies/run_command_cd_instead_of_workdir.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS013 2 | 3 | import data.lib.docker 4 | 5 | __rego_metadata__ := { 6 | "id": "DS013", 7 | "avd_id": "AVD-DS-0013", 8 | "title": "'RUN cd ...' to change directory", 9 | "short_code": "use-workdir-over-cd", 10 | "version": "v1.0.0", 11 | "severity": "MEDIUM", 12 | "type": "Dockerfile Security Check", 13 | "description": "Use WORKDIR instead of proliferating instructions like 'RUN cd … && do-something', which are hard to read, troubleshoot, and maintain.", 14 | "recommended_actions": "Use WORKDIR to change directory", 15 | "url": "https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#workdir", 16 | } 17 | 18 | __rego_input__ := { 19 | "combine": false, 20 | "selector": [{"type": "dockerfile"}], 21 | } 22 | 23 | get_cd[args] { 24 | run := docker.run[_] 25 | parts = regex.split(`\s*&&\s*`, run.Value[_]) 26 | startswith(parts[_], "cd ") 27 | args := concat(" ", run.Value) 28 | } 29 | 30 | deny[res] { 31 | args := get_cd[_] 32 | res := sprintf("RUN should not be used to change directory: '%s'. Use 'WORKDIR' statement instead.", [args]) 33 | } 34 | -------------------------------------------------------------------------------- /docker/policies/run_command_cd_instead_of_workdir_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS013 2 | 3 | test_basic_denied { 4 | r := deny with input as {"stages": {"nginx": [ 5 | { 6 | "Cmd": "from", 7 | "Value": ["nginx"], 8 | }, 9 | { 10 | "Cmd": "run", 11 | "Value": ["cd /usr/share/nginx/html"], 12 | }, 13 | { 14 | "Cmd": "cmd", 15 | "Value": ["cd /usr/share/nginx/html && sed -e s/Docker/\"$AUTHOR\"/ Hello_docker.html > index.html ; nginx -g 'daemon off;'"], 16 | }, 17 | ]}} 18 | 19 | count(r) == 1 20 | r[_] == "RUN should not be used to change directory: 'cd /usr/share/nginx/html'. Use 'WORKDIR' statement instead." 21 | } 22 | 23 | test_chaining_denied { 24 | r := deny with input as {"stages": {"nginx": [ 25 | { 26 | "Cmd": "from", 27 | "Value": ["nginx"], 28 | }, 29 | { 30 | "Cmd": "env", 31 | "Value": [ 32 | "AUTHOR", 33 | "Docker", 34 | ], 35 | }, 36 | { 37 | "Cmd": "run", 38 | "Value": ["apt-get install vim && cd /usr/share/nginx/html"], 39 | }, 40 | { 41 | "Cmd": "cmd", 42 | "Value": ["cd /usr/share/nginx/html && sed -e s/Docker/\"$AUTHOR\"/ Hello_docker.html > index.html ; nginx -g 'daemon off;'"], 43 | }, 44 | ]}} 45 | 46 | count(r) == 1 47 | r[_] == "RUN should not be used to change directory: 'apt-get install vim && cd /usr/share/nginx/html'. Use 'WORKDIR' statement instead." 48 | } 49 | 50 | test_basic_allowed { 51 | r := deny with input as {"stages": {"nginx": [ 52 | { 53 | "Cmd": "from", 54 | "Value": ["nginx"], 55 | }, 56 | { 57 | "Cmd": "workdir", 58 | "Value": ["/usr/share/nginx/html"], 59 | }, 60 | { 61 | "Cmd": "copy", 62 | "Value": [ 63 | "Hello_docker.html", 64 | "/usr/share/nginx/html", 65 | ], 66 | }, 67 | { 68 | "Cmd": "cmd", 69 | "Value": ["cd /usr/share/nginx/html && sed -e s/Docker/\"$AUTHOR\"/ Hello_docker.html > index.html ; nginx -g 'daemon off;'"], 70 | }, 71 | ]}} 72 | 73 | count(r) == 0 74 | } 75 | -------------------------------------------------------------------------------- /docker/policies/run_using_sudo.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS010 2 | 3 | import data.lib.docker 4 | 5 | __rego_metadata__ := { 6 | "id": "DS010", 7 | "avd_id": "AVD-DS-0010", 8 | "title": "RUN using 'sudo'", 9 | "short_code": "no-sudo-run", 10 | "version": "v1.0.0", 11 | "severity": "CRITICAL", 12 | "type": "Dockerfile Security Check", 13 | "description": "Avoid using 'RUN' with 'sudo' commands, as it can lead to unpredictable behavior.", 14 | "recommended_actions": "Don't use sudo", 15 | "url": "https://docs.docker.com/engine/reference/builder/#run", 16 | } 17 | 18 | __rego_input__ := { 19 | "combine": false, 20 | "selector": [{"type": "dockerfile"}], 21 | } 22 | 23 | has_sudo(commands) { 24 | parts = split(commands, "&&") 25 | 26 | instruction := parts[_] 27 | regex.match(`^\s*sudo`, instruction) 28 | } 29 | 30 | get_sudo[arg] { 31 | run = docker.run[_] 32 | count(run.Value) == 1 33 | 34 | arg := run.Value[0] 35 | 36 | has_sudo(arg) 37 | } 38 | 39 | deny[res] { 40 | count(get_sudo) > 0 41 | res := "Using 'sudo' in Dockerfile should be avoided" 42 | } 43 | -------------------------------------------------------------------------------- /docker/policies/run_using_sudo_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS010 2 | 3 | test_basic_denied { 4 | r := deny with input as {"stages": {"alpine:3.5": [ 5 | { 6 | "Cmd": "from", 7 | "Value": ["alpine:3.5"], 8 | }, 9 | { 10 | "Cmd": "run", 11 | "Value": ["apk add --update py2-pip"], 12 | }, 13 | { 14 | "Cmd": "run", 15 | "Value": ["sudo pip install --upgrade pip"], 16 | }, 17 | { 18 | "Cmd": "cmd", 19 | "Value": [ 20 | "python", 21 | "/usr/src/app/app.py", 22 | ], 23 | }, 24 | ]}} 25 | 26 | count(r) == 1 27 | r[_] == "Using 'sudo' in Dockerfile should be avoided" 28 | } 29 | 30 | test_chaining_denied { 31 | r := deny with input as {"stages": {"alpine:3.5": [ 32 | { 33 | "Cmd": "from", 34 | "Value": ["alpine:3.5"], 35 | }, 36 | { 37 | "Cmd": "run", 38 | "Value": ["RUN apk add bash && sudo pip install --upgrade pip"], 39 | }, 40 | ]}} 41 | 42 | count(r) == 1 43 | r[_] == "Using 'sudo' in Dockerfile should be avoided" 44 | } 45 | 46 | test_multi_vuls_denied { 47 | r := deny with input as {"stages": {"alpine:3.5": [ 48 | { 49 | "Cmd": "from", 50 | "Value": ["alpine:3.5"], 51 | }, 52 | { 53 | "Cmd": "run", 54 | "Value": ["RUN sudo pip install --upgrade pip"], 55 | }, 56 | { 57 | "Cmd": "run", 58 | "Value": ["RUN apk add bash && sudo pip install --upgrade pip"], 59 | }, 60 | ]}} 61 | 62 | count(r) == 1 63 | r[_] == "Using 'sudo' in Dockerfile should be avoided" 64 | } 65 | 66 | test_basic_allowed { 67 | r := deny with input as {"stages": {"alpine:3.3": [ 68 | { 69 | "Cmd": "from", 70 | "Value": ["alpine:3.5"], 71 | }, 72 | { 73 | "Cmd": "run", 74 | "Value": ["apk add --update py2-pip"], 75 | }, 76 | { 77 | "Cmd": "run", 78 | "Value": ["apt-get install sudo"], 79 | }, 80 | { 81 | "Cmd": "cmd", 82 | "Value": ["python", "/usr/src/app/app.py"], 83 | }, 84 | ]}} 85 | 86 | count(r) == 0 87 | } 88 | -------------------------------------------------------------------------------- /docker/policies/run_using_wget_and_curl.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS014 2 | 3 | import data.lib.docker 4 | 5 | __rego_metadata__ := { 6 | "id": "DS014", 7 | "avd_id": "AVD-DS-0014", 8 | "title": "RUN using 'wget' and 'curl'", 9 | "short_code": "standardise-remote-get", 10 | "version": "v1.0.0", 11 | "severity": "LOW", 12 | "type": "Dockerfile Security Check", 13 | "description": "Avoid using both 'wget' and 'curl' since these tools have the same effect.", 14 | "recommended_actions": "Pick one util, either 'wget' or 'curl'", 15 | "url": "https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#run", 16 | } 17 | 18 | __rego_input__ := { 19 | "combine": false, 20 | "selector": [{"type": "dockerfile"}], 21 | } 22 | 23 | deny[res] { 24 | wget := get_tool_usage(docker.run[_], "wget") 25 | curl := get_tool_usage(docker.run[_], "curl") 26 | 27 | count(wget) > 0 28 | count(curl) > 0 29 | 30 | res := "Shouldn't use both curl and wget" 31 | } 32 | 33 | # chained commands 34 | # e.g. RUN curl http://example.com 35 | get_tool_usage(cmd, cmd_name) = r { 36 | count(cmd.Value) == 1 37 | 38 | commands_list = split(cmd.Value[0], "&&") 39 | 40 | reg_exp = sprintf("^( )*%s", [cmd_name]) 41 | 42 | r := [x | 43 | instruction := commands_list[_] 44 | 45 | #install is allowed (it may be required by installed app) 46 | not contains(instruction, "install ") 47 | regex.match(reg_exp, instruction) 48 | x := cmd.Value[0] 49 | ] 50 | } 51 | 52 | # JSON array is specified 53 | # e.g. RUN ["curl", "http://example.com"] 54 | get_tool_usage(cmd, cmd_name) = res { 55 | count(cmd.Value) > 1 56 | 57 | cmd.Value[0] == cmd_name 58 | 59 | res := [concat(" ", cmd.Value)] 60 | } 61 | -------------------------------------------------------------------------------- /docker/policies/run_using_wget_and_curl_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS014 2 | 3 | test_basic_denied { 4 | r := deny with input as {"stages": {"alpine:3.5": [ 5 | { 6 | "Cmd": "from", 7 | "Value": ["debian"], 8 | }, 9 | { 10 | "Cmd": "run", 11 | "Value": ["wget http://google.com"], 12 | }, 13 | { 14 | "Cmd": "run", 15 | "Value": ["curl http://bing.com"], 16 | }, 17 | ]}} 18 | 19 | count(r) == 1 20 | r[_] == "Shouldn't use both curl and wget" 21 | } 22 | 23 | test_json_array_denied { 24 | r := deny with input as {"stages": {"alpine:3.5": [ 25 | { 26 | "Cmd": "from", 27 | "Value": ["baseImage"], 28 | }, 29 | { 30 | "Cmd": "run", 31 | "Value": ["wget http://google.com"], 32 | }, 33 | { 34 | "Cmd": "run", 35 | "Value": [ 36 | "curl", 37 | "http://bing.com", 38 | ], 39 | }, 40 | ]}} 41 | 42 | count(r) == 1 43 | r[_] == "Shouldn't use both curl and wget" 44 | } 45 | 46 | test_basic_allowed { 47 | r := deny with input as {"stages": { 48 | "alpine:3.5": [ 49 | { 50 | "Cmd": "from", 51 | "Value": ["debian"], 52 | }, 53 | { 54 | "Cmd": "run", 55 | "Value": ["curl http://bing.com"], 56 | }, 57 | { 58 | "Cmd": "run", 59 | "Value": ["curl http://google.com"], 60 | }, 61 | ], 62 | "baseimage:1.0": [ 63 | { 64 | "Cmd": "from", 65 | "Value": ["baseImage"], 66 | }, 67 | { 68 | "Cmd": "run", 69 | "Value": [ 70 | "curl", 71 | "http://bing.com", 72 | ], 73 | }, 74 | ], 75 | }} 76 | 77 | count(r) == 0 78 | } 79 | 80 | test_json_array_allowed { 81 | r := deny with input as {"stages": {"alpine:3.5": [ 82 | { 83 | "Cmd": "from", 84 | "Value": ["debian"], 85 | }, 86 | { 87 | "Cmd": "run", 88 | "Value": ["curl", "http://bing.com"], 89 | }, 90 | { 91 | "Cmd": "run", 92 | "Value": [ 93 | "curl", 94 | "http://google.com", 95 | ], 96 | }, 97 | ]}} 98 | 99 | count(r) == 0 100 | } 101 | 102 | test_install_allowed { 103 | r := deny with input as {"stages": {"alpine:3.5": [ 104 | { 105 | "Cmd": "from", 106 | "Value": ["debian"], 107 | }, 108 | { 109 | "Cmd": "run", 110 | "Value": ["curl http://bing.com"], 111 | }, 112 | { 113 | "Cmd": "run", 114 | "Value": ["apt-get update && apt-get install wget"], 115 | }, 116 | ]}} 117 | 118 | count(r) == 0 119 | } 120 | -------------------------------------------------------------------------------- /docker/policies/same_alias_in_different_froms.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS012 2 | 3 | __rego_metadata__ := { 4 | "id": "DS012", 5 | "avd_id": "AVD-DS-0012", 6 | "title": "Duplicate aliases defined in different FROMs", 7 | "short_code": "no-duplicate-alias", 8 | "version": "v1.0.0", 9 | "severity": "CRITICAL", 10 | "type": "Dockerfile Security Check", 11 | "description": "Different FROMs can't have the same alias defined.", 12 | "recommended_actions": "Change aliases to make them different", 13 | "url": "https://docs.docker.com/develop/develop-images/multistage-build/", 14 | } 15 | 16 | __rego_input__ := { 17 | "combine": false, 18 | "selector": [{"type": "dockerfile"}], 19 | } 20 | 21 | get_duplicate_alias[alias1] { 22 | name1 := get_aliased_name[_] 23 | name2 := get_aliased_name[_] 24 | name1 != name2 25 | 26 | [_, alias1] := regex.split(`\s+as\s+`, name1) 27 | [_, alias2] := regex.split(`\s+as\s+`, name2) 28 | alias1 == alias2 29 | } 30 | 31 | get_aliased_name[arg] { 32 | some name 33 | input.stages[name] 34 | 35 | arg = lower(name) 36 | contains(arg, " as ") 37 | } 38 | 39 | deny[res] { 40 | alias := get_duplicate_alias[_] 41 | res := sprintf("Duplicate aliases '%s' are found in different FROMs", [alias]) 42 | } 43 | -------------------------------------------------------------------------------- /docker/policies/unix_ports_out_of_range.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS008 2 | 3 | import data.lib.docker 4 | 5 | __rego_metadata__ := { 6 | "id": "DS008", 7 | "avd_id": "AVD-DS-0008", 8 | "title": "Exposed port out of range", 9 | "short_code": "port-out-of-range", 10 | "version": "v1.0.0", 11 | "severity": "CRITICAL", 12 | "type": "Dockerfile Security Check", 13 | "description": "UNIX ports outside the range 0-65535 are exposed.", 14 | "recommended_actions": "Use port number within range", 15 | "url": "https://docs.docker.com/engine/reference/builder/#expose", 16 | } 17 | 18 | __rego_input__ := { 19 | "combine": false, 20 | "selector": [{"type": "dockerfile"}], 21 | } 22 | 23 | invalid_ports[port] { 24 | expose := docker.expose[_] 25 | port := to_number(split(expose.Value[_], "/")[0]) 26 | port > 65535 27 | } 28 | 29 | deny[res] { 30 | port := invalid_ports[_] 31 | res := sprintf("'EXPOSE' contains port which is out of range [0, 65535]: %d", [port]) 32 | } 33 | -------------------------------------------------------------------------------- /docker/policies/unix_ports_out_of_range_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS008 2 | 3 | test_denied { 4 | r := deny with input as {"stages": {"alpine:3.3": [ 5 | { 6 | "Cmd": "from", 7 | "Value": ["alpine:3.3"], 8 | }, 9 | { 10 | "Cmd": "run", 11 | "Value": ["apk --no-cache add nginx"], 12 | }, 13 | { 14 | "Cmd": "expose", 15 | "Value": [ 16 | "65536/tcp", 17 | "80", 18 | "443", 19 | "22", 20 | ], 21 | }, 22 | { 23 | "Cmd": "cmd", 24 | "Value": [ 25 | "nginx", 26 | "-g", 27 | "daemon off;", 28 | ], 29 | }, 30 | ]}} 31 | 32 | count(r) == 1 33 | r[_] == "'EXPOSE' contains port which is out of range [0, 65535]: 65536" 34 | } 35 | 36 | test_allowed { 37 | r := deny with input as {"stages": {"alpine:3.3": [ 38 | { 39 | "Cmd": "from", 40 | "Value": ["alpine:3.3"], 41 | }, 42 | { 43 | "Cmd": "run", 44 | "Value": ["apk --no-cache add nginx"], 45 | }, 46 | { 47 | "Cmd": "expose", 48 | "Value": [ 49 | "65530/tcp", 50 | "80", 51 | "443", 52 | "22", 53 | ], 54 | }, 55 | { 56 | "Cmd": "cmd", 57 | "Value": [ 58 | "nginx", 59 | "-g", 60 | "daemon off;", 61 | ], 62 | }, 63 | ]}} 64 | 65 | count(r) == 0 66 | } 67 | -------------------------------------------------------------------------------- /docker/policies/update_instruction_alone.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS017 2 | 3 | import data.lib.docker 4 | 5 | __rego_metadata__ := { 6 | "id": "DS017", 7 | "avd_id": "AVD-DS-0017", 8 | "title": "'RUN update' instruction alone", 9 | "short_code": "no-orphan-package-update", 10 | "version": "v1.0.0", 11 | "severity": "HIGH", 12 | "type": "Dockerfile Security Check", 13 | "description": "The instruction 'RUN update' should always be followed by ' install' in the same RUN statement.", 14 | "recommended_actions": "Combine ' update' and ' install' instructions to single one", 15 | "url": "https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#run", 16 | } 17 | 18 | __rego_input__ := { 19 | "combine": false, 20 | "selector": [{"type": "dockerfile"}], 21 | } 22 | 23 | deny[res] { 24 | run := docker.run[_] 25 | 26 | command = concat(" ", run.Value) 27 | 28 | is_valid_update(command) 29 | not update_followed_by_install(command) 30 | 31 | res := __rego_metadata__.description 32 | } 33 | 34 | is_valid_update(command) { 35 | chained_parts := regex.split(`\s*&&\s*`, command) 36 | 37 | array_split := split(chained_parts[_], " ") 38 | 39 | len = count(array_split) 40 | 41 | update := {"update", "--update"} 42 | 43 | array_split[len - 1] == update[_] 44 | } 45 | 46 | update_followed_by_install(command) { 47 | command_list = [ 48 | "install", 49 | "source-install", 50 | "reinstall", 51 | "groupinstall", 52 | "localinstall", 53 | "apk add", 54 | ] 55 | 56 | update := indexof(command, "update") 57 | update != -1 58 | 59 | install := indexof(command, command_list[_]) 60 | install != -1 61 | 62 | update < install 63 | } 64 | -------------------------------------------------------------------------------- /docker/policies/update_instruction_alone_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS017 2 | 3 | test_denied { 4 | r := deny with input as {"stages": {"ubuntu:18.04": [ 5 | { 6 | "Cmd": "from", 7 | "Value": ["ubuntu:18.04"], 8 | }, 9 | { 10 | "Cmd": "run", 11 | "Value": ["apt-get update"], 12 | }, 13 | { 14 | "Cmd": "run", 15 | "Value": ["apt-get install -y --no-install-recommends mysql-client && rm -rf /var/lib/apt/lists/*"], 16 | }, 17 | { 18 | "Cmd": "entrypoint", 19 | "Value": ["mysql"], 20 | }, 21 | ]}} 22 | 23 | count(r) == 1 24 | trace(sprintf("%s", [r[_]])) 25 | r[_] == "The instruction 'RUN update' should always be followed by ' install' in the same RUN statement." 26 | } 27 | 28 | test_json_array_denied { 29 | r := deny with input as {"stages": {"ubuntu:18.04": [ 30 | { 31 | "Cmd": "from", 32 | "Value": ["ubuntu:18.04"], 33 | }, 34 | { 35 | "Cmd": "run", 36 | "Value": ["apt-get", "update"], 37 | }, 38 | { 39 | "Cmd": "entrypoint", 40 | "Value": ["mysql"], 41 | }, 42 | ]}} 43 | 44 | count(r) == 1 45 | r[_] == "The instruction 'RUN update' should always be followed by ' install' in the same RUN statement." 46 | } 47 | 48 | test_chained_denied { 49 | r := deny with input as {"stages": {"ubuntu:18.04": [ 50 | { 51 | "Cmd": "from", 52 | "Value": ["ubuntu:18.04"], 53 | }, 54 | { 55 | "Cmd": "run", 56 | "Value": ["apt-get update && adduser mike"], 57 | }, 58 | { 59 | "Cmd": "run", 60 | "Value": ["apt-get install -y --no-install-recommends mysql-client && rm -rf /var/lib/apt/lists/*"], 61 | }, 62 | { 63 | "Cmd": "entrypoint", 64 | "Value": ["mysql"], 65 | }, 66 | ]}} 67 | 68 | count(r) == 1 69 | r[_] == "The instruction 'RUN update' should always be followed by ' install' in the same RUN statement." 70 | } 71 | 72 | test_allowed { 73 | r := deny with input as {"stages": {"ubuntu:18.04": [ 74 | { 75 | "Cmd": "from", 76 | "Value": ["ubuntu:18.04"], 77 | }, 78 | { 79 | "Cmd": "run", 80 | "Value": ["apt-get update && apt-get install -y --no-install-recommends mysql-client && rm -rf /var/lib/apt/lists/*"], 81 | }, 82 | { 83 | "Cmd": "run", 84 | "Value": ["apk update && apk add --no-cache git ca-certificates"], 85 | }, 86 | { 87 | "Cmd": "run", 88 | "Value": ["apk --update add easy-rsa"], 89 | }, 90 | { 91 | "Cmd": "entrypoint", 92 | "Value": ["mysql"], 93 | }, 94 | ]}} 95 | 96 | count(r) == 0 97 | } 98 | -------------------------------------------------------------------------------- /docker/policies/workdir_path_not_absolute.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS009 2 | 3 | import data.lib.docker 4 | 5 | __rego_metadata__ := { 6 | "id": "DS009", 7 | "avd_id": "AVD-DS-0009", 8 | "title": "WORKDIR path not absolute", 9 | "short_code": "user-absolute-workdir", 10 | "version": "v1.0.0", 11 | "severity": "HIGH", 12 | "type": "Dockerfile Security Check", 13 | "description": "For clarity and reliability, you should always use absolute paths for your WORKDIR.", 14 | "recommended_actions": "Use absolute paths for your WORKDIR", 15 | "url": "https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#workdir", 16 | } 17 | 18 | __rego_input__ := { 19 | "combine": false, 20 | "selector": [{"type": "dockerfile"}], 21 | } 22 | 23 | get_work_dir[arg] { 24 | workdir := docker.workdir[_] 25 | arg := workdir.Value[0] 26 | 27 | not regex.match("(^/[A-z0-9-_+]*)|(^[A-z0-9-_+]:\\\\.*)|(^\\$[{}A-z0-9-_+].*)", arg) 28 | } 29 | 30 | deny[res] { 31 | arg := get_work_dir[_] 32 | res := sprintf("WORKDIR path '%s' should be absolute", [arg]) 33 | } 34 | -------------------------------------------------------------------------------- /docker/policies/workdir_path_not_absolute_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS009 2 | 3 | test_basic_denied { 4 | r := deny with input as {"stages": {"alpine:3.5": [ 5 | {"Cmd": "from", "Value": ["alpine:3.5"]}, 6 | { 7 | "Cmd": "run", 8 | "Value": ["apk add --update py2-pip"], 9 | }, 10 | { 11 | "Cmd": "workdir", 12 | "Value": ["/path/to/workdir"], 13 | }, 14 | { 15 | "Cmd": "workdir", 16 | "Value": ["workdir"], 17 | }, 18 | ]}} 19 | 20 | count(r) == 1 21 | r[_] == "WORKDIR path 'workdir' should be absolute" 22 | } 23 | 24 | test_no_work_dir_allowed { 25 | r := deny with input as {"stages": {"alpine:3.3": [ 26 | { 27 | "Cmd": "from", 28 | "Value": ["alpine:3.3"], 29 | }, 30 | { 31 | "Cmd": "run", 32 | "Value": ["apk --no-cache add nginx"], 33 | }, 34 | ]}} 35 | 36 | count(r) == 0 37 | } 38 | 39 | test_absolute_work_dir_allowed { 40 | r := deny with input as {"stages": {"alpine:3.3": [ 41 | { 42 | "Cmd": "from", 43 | "Value": ["alpine:3.3"], 44 | }, 45 | { 46 | "Cmd": "run", 47 | "Value": ["apk --no-cache add nginx"], 48 | }, 49 | { 50 | "Cmd": "workdir", 51 | "Value": ["/path/to/workdir"], 52 | }, 53 | ]}} 54 | 55 | count(r) == 0 56 | } 57 | -------------------------------------------------------------------------------- /docker/policies/yum_clean_all_missing.rego: -------------------------------------------------------------------------------- 1 | package appshield.dockerfile.DS015 2 | 3 | import data.lib.docker 4 | 5 | __rego_metadata__ := { 6 | "id": "DS015", 7 | "avd_id": "AVD-DS-0015", 8 | "title": "'yum clean all' missing", 9 | "short_code": "purge-yum-package-cache", 10 | "version": "v1.0.0", 11 | "severity": "HIGH", 12 | "type": "Dockerfile Security Check", 13 | "description": "You should use 'yum clean all' after using a 'yum install' command to clean package cached data and reduce image size.", 14 | "recommended_actions": "Add 'yum clean all' to Dockerfile", 15 | "url": "https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#run", 16 | } 17 | 18 | __rego_input__ := { 19 | "combine": false, 20 | "selector": [{"type": "dockerfile"}], 21 | } 22 | 23 | get_yum[arg] { 24 | run := docker.run[_] 25 | arg := run.Value[0] 26 | 27 | regex.match("yum (-[a-zA-Z]+ *)*install", arg) 28 | 29 | not contains_clean_after_yum(arg) 30 | } 31 | 32 | deny[res] { 33 | args := get_yum[_] 34 | res := sprintf("'yum clean all' is missed: %s", [args]) 35 | } 36 | 37 | contains_clean_after_yum(cmd) { 38 | yum_commands := regex.find_n("(yum (-[a-zA-Z]+ *)*install)|(yum clean all)", cmd, -1) 39 | 40 | yum_commands[count(yum_commands) - 1] == "yum clean all" 41 | } 42 | -------------------------------------------------------------------------------- /docker/test/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk 2 | 3 | VOLUME /tmp 4 | ARG DEPENDENCY=target/dependency 5 | COPY ${DEPENDENCY}/BOOT-INF/lib /app/lib 6 | COPY ${DEPENDENCY}/META-INF /app/META-INF 7 | COPY ${DEPENDENCY}/BOOT-INF/classes /app 8 | 9 | RUN apk add --no-cache python3 python3-dev build-base && pip3 install awscli==1.18.1 10 | 11 | ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"] 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aquasecurity/appshield 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/aquasecurity/fanal v0.0.0-20210710080753-f728f1973410 7 | github.com/stretchr/testify v1.7.0 8 | ) 9 | -------------------------------------------------------------------------------- /integration/testdata/DS001/Dockerfile.allowed: -------------------------------------------------------------------------------- 1 | FROM debian:9 2 | RUN apt-get update && apt-get -y install vim && apt-get clean 3 | USER foo -------------------------------------------------------------------------------- /integration/testdata/DS001/Dockerfile.denied: -------------------------------------------------------------------------------- 1 | FROM debian:latest 2 | RUN apt-get update && apt-get -y install vim && apt-get clean 3 | USER foo 4 | -------------------------------------------------------------------------------- /integration/testdata/DS002/Dockerfile.allowed: -------------------------------------------------------------------------------- 1 | FROM debian:9 2 | RUN apt-get update && apt-get -y install vim && apt-get clean 3 | USER foo -------------------------------------------------------------------------------- /integration/testdata/DS002/Dockerfile.denied: -------------------------------------------------------------------------------- 1 | FROM debian:9 2 | RUN apt-get update && apt-get -y install vim && apt-get clean 3 | -------------------------------------------------------------------------------- /integration/testdata/DS003/Dockerfile.allowed: -------------------------------------------------------------------------------- 1 | FROM alpine:3.13 2 | USER mike 3 | RUN apt-get install -y curl && apt-get clean 4 | -------------------------------------------------------------------------------- /integration/testdata/DS003/Dockerfile.denied: -------------------------------------------------------------------------------- 1 | FROM alpine:3.13 2 | USER mike 3 | RUN apt-get install -y curl 4 | -------------------------------------------------------------------------------- /integration/testdata/DS004/Dockerfile.allowed: -------------------------------------------------------------------------------- 1 | FROM alpine:3.13 2 | USER mike 3 | EXPOSE 8080 4 | -------------------------------------------------------------------------------- /integration/testdata/DS004/Dockerfile.denied: -------------------------------------------------------------------------------- 1 | FROM alpine:3.13 2 | USER mike 3 | EXPOSE 22 -------------------------------------------------------------------------------- /integration/testdata/DS005/Dockerfile.allowed: -------------------------------------------------------------------------------- 1 | FROM alpine:3.13 2 | USER mike 3 | ADD "/target/resources.tar.gz" "resources" 4 | -------------------------------------------------------------------------------- /integration/testdata/DS005/Dockerfile.denied: -------------------------------------------------------------------------------- 1 | FROM alpine:3.13 2 | USER mike 3 | ADD "/target/resources.tar.gz" "resources.jar" 4 | ADD "/target/app.jar" "app.jar" -------------------------------------------------------------------------------- /integration/testdata/DS006/Dockerfile.allowed: -------------------------------------------------------------------------------- 1 | FROM golang:1.7.3 as dep 2 | COPY /binary / 3 | 4 | FROM alpine:3.13 5 | USER mike 6 | ENTRYPOINT [ "/opt/app/run.sh --port 8080" ] -------------------------------------------------------------------------------- /integration/testdata/DS006/Dockerfile.denied: -------------------------------------------------------------------------------- 1 | FROM golang:1.7.3 as dep 2 | COPY --from=dep /binary / 3 | 4 | FROM alpine:3.13 5 | USER mike 6 | ENTRYPOINT [ "/opt/app/run.sh --port 8080" ] -------------------------------------------------------------------------------- /integration/testdata/DS007/Dockerfile.allowed: -------------------------------------------------------------------------------- 1 | FROM golang:1.7.3 as dep 2 | COPY /binary / 3 | 4 | FROM alpine:3.13 5 | USER mike 6 | ENTRYPOINT [ "/opt/app/run.sh --port 8080" ] -------------------------------------------------------------------------------- /integration/testdata/DS007/Dockerfile.denied: -------------------------------------------------------------------------------- 1 | FROM golang:1.7.3 as dep 2 | COPY dep /binary / 3 | ENTRYPOINT [ "/opt/app/run.sh --port 8080" ] 4 | ENTRYPOINT [ "/opt/app/run.sh --port 8080" ] 5 | 6 | FROM alpine:3.13 7 | USER mike 8 | ENTRYPOINT [ "/opt/app/run.sh --port 8080" ] -------------------------------------------------------------------------------- /integration/testdata/DS008/Dockerfile.allowed: -------------------------------------------------------------------------------- 1 | FROM alpine:3.13 2 | USER mike 3 | EXPOSE 65530 8080 4 | -------------------------------------------------------------------------------- /integration/testdata/DS008/Dockerfile.denied: -------------------------------------------------------------------------------- 1 | FROM alpine:3.13 2 | USER mike 3 | EXPOSE 65536 8080 4 | -------------------------------------------------------------------------------- /integration/testdata/DS009/Dockerfile.allowed: -------------------------------------------------------------------------------- 1 | FROM alpine:3.13 2 | USER mike 3 | WORKDIR /path/to/workdir 4 | -------------------------------------------------------------------------------- /integration/testdata/DS009/Dockerfile.denied: -------------------------------------------------------------------------------- 1 | FROM alpine:3.13 2 | USER mike 3 | WORKDIR path/to/workdir 4 | -------------------------------------------------------------------------------- /integration/testdata/DS010/Dockerfile.allowed: -------------------------------------------------------------------------------- 1 | FROM alpine:3.13 2 | RUN pip install --upgrade pip 3 | USER mike 4 | -------------------------------------------------------------------------------- /integration/testdata/DS010/Dockerfile.denied: -------------------------------------------------------------------------------- 1 | FROM alpine:3.13 2 | RUN sudo pip install --upgrade pip 3 | USER mike 4 | -------------------------------------------------------------------------------- /integration/testdata/DS011/Dockerfile.allowed: -------------------------------------------------------------------------------- 1 | FROM alpine:3.13 2 | USER mike 3 | COPY ["package.json", "yarn.lock", "myapp/"] 4 | -------------------------------------------------------------------------------- /integration/testdata/DS011/Dockerfile.denied: -------------------------------------------------------------------------------- 1 | FROM alpine:3.13 2 | USER mike 3 | COPY ["package.json", "yarn.lock", "myapp"] 4 | -------------------------------------------------------------------------------- /integration/testdata/DS012/Dockerfile.allowed: -------------------------------------------------------------------------------- 1 | FROM baseImage:1.1 2 | RUN test 3 | 4 | FROM debian:jesse2 as build2 5 | USER mike 6 | RUN stuff 7 | 8 | FROM debian:jesse1 as build1 9 | USER mike 10 | RUN more_stuff -------------------------------------------------------------------------------- /integration/testdata/DS012/Dockerfile.denied: -------------------------------------------------------------------------------- 1 | FROM baseImage:1.1 2 | RUN test 3 | 4 | FROM debian:jesse2 as build 5 | USER mike 6 | RUN stuff 7 | 8 | FROM debian:jesse1 as build 9 | USER mike 10 | RUN more_stuff -------------------------------------------------------------------------------- /integration/testdata/DS013/Dockerfile.allowed: -------------------------------------------------------------------------------- 1 | FROM nginx:2.2 2 | WORKDIR /usr/share/nginx/html 3 | USER mike 4 | CMD cd /usr/share/nginx/html && sed -e s/Docker/\"$AUTHOR\"/ Hello_docker.html > index.html ; nginx -g 'daemon off;' -------------------------------------------------------------------------------- /integration/testdata/DS013/Dockerfile.denied: -------------------------------------------------------------------------------- 1 | FROM nginx:2.2 2 | RUN cd /usr/share/nginx/html 3 | USER mike 4 | CMD cd /usr/share/nginx/html && sed -e s/Docker/\"$AUTHOR\"/ Hello_docker.html > index.html ; nginx -g 'daemon off;' -------------------------------------------------------------------------------- /integration/testdata/DS014/Dockerfile.allowed: -------------------------------------------------------------------------------- 1 | FROM debian:stable-20210621 2 | RUN curl http://bing.com 3 | RUN curl http://google.com 4 | 5 | FROM baseimage:1.0 6 | USER mike 7 | RUN curl http://bing.com 8 | -------------------------------------------------------------------------------- /integration/testdata/DS014/Dockerfile.denied: -------------------------------------------------------------------------------- 1 | FROM debian:stable-20210621 2 | RUN wget http://bing.com 3 | RUN curl http://google.com 4 | 5 | FROM baseimage:1.0 6 | USER mike 7 | RUN curl http://bing.com 8 | -------------------------------------------------------------------------------- /integration/testdata/DS015/Dockerfile.allowed: -------------------------------------------------------------------------------- 1 | FROM alpine:3.5 2 | RUN yum install && yum clean all 3 | RUN pip install --no-cache-dir -r /usr/src/app/requirements.txt 4 | USER mike 5 | CMD python /usr/src/app/app.py -------------------------------------------------------------------------------- /integration/testdata/DS015/Dockerfile.denied: -------------------------------------------------------------------------------- 1 | FROM alpine:3.5 2 | RUN yum install vim 3 | RUN pip install --no-cache-dir -r /usr/src/app/requirements.txt 4 | USER mike 5 | CMD python /usr/src/app/app.py -------------------------------------------------------------------------------- /integration/testdata/DS016/Dockerfile.allowed: -------------------------------------------------------------------------------- 1 | FROM golang:1.7.3 2 | USER mike 3 | CMD ./apps 4 | FROM alpine:3.13 5 | CMD ./app 6 | -------------------------------------------------------------------------------- /integration/testdata/DS016/Dockerfile.denied: -------------------------------------------------------------------------------- 1 | FROM golang:1.7.3 2 | USER mike 3 | CMD ./app 4 | CMD ./apps 5 | FROM alpine:3.13 6 | CMD ./app 7 | -------------------------------------------------------------------------------- /integration/testdata/DS017/Dockerfile.allowed: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | RUN apt-get update && apt-get install -y --no-install-recommends mysql-client && rm -rf /var/lib/apt/lists/* && apt-get clean 3 | USER mike 4 | ENTRYPOINT mysql -------------------------------------------------------------------------------- /integration/testdata/DS017/Dockerfile.denied: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | RUN apt-get update 3 | RUN apt-get install -y --no-install-recommends mysql-client && rm -rf /var/lib/apt/lists/* && apt-get clean 4 | USER mike 5 | ENTRYPOINT mysql -------------------------------------------------------------------------------- /integration/testdata/DS018/Dockerfile.allowed: -------------------------------------------------------------------------------- 1 | FROM golang:1.7.3 AS builder 2 | RUN command CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . 3 | 4 | FROM alpine:3.13 5 | COPY --from=builder /go/src/github.com/alexellis/href-counter/app . 6 | USER mike 7 | CMD ./app -------------------------------------------------------------------------------- /integration/testdata/DS018/Dockerfile.denied: -------------------------------------------------------------------------------- 1 | FROM golang:1.7.3 AS builder 2 | RUN command CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . 3 | 4 | FROM alpine:3.13 5 | COPY --from=dep /go/src/github.com/alexellis/href-counter/app . 6 | USER mike 7 | CMD ./app -------------------------------------------------------------------------------- /integration/testdata/DS019/Dockerfile.allowed: -------------------------------------------------------------------------------- 1 | FROM fedora:27 2 | USER mike 3 | RUN set -uex && dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo && sed -i 's/\\$releasever/26/g' /etc/yum.repos.d/docker-ce.repo && dnf install -vy docker-ce && dnf clean all 4 | HEALTHCHECK CMD curl --fail http://localhost:3000 || exit 1 5 | 6 | -------------------------------------------------------------------------------- /integration/testdata/DS019/Dockerfile.denied: -------------------------------------------------------------------------------- 1 | FROM fedora:27 2 | USER mike 3 | RUN set -uex && dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo && sed -i 's/\\$releasever/26/g' /etc/yum.repos.d/docker-ce.repo && dnf install -vy docker-ce 4 | HEALTHCHECK CMD curl --fail http://localhost:3000 || exit 1 5 | -------------------------------------------------------------------------------- /integration/testdata/DS020/Dockerfile.allowed: -------------------------------------------------------------------------------- 1 | FROM alpine:3.5 2 | RUN zypper install bash && zypper clean 3 | RUN pip install --no-cache-dir -r /usr/src/app/requirements.txt 4 | USER mike 5 | CMD python /usr/src/app/app.py -------------------------------------------------------------------------------- /integration/testdata/DS020/Dockerfile.denied: -------------------------------------------------------------------------------- 1 | FROM alpine:3.5 2 | RUN zypper install bash 3 | RUN pip install --no-cache-dir -r /usr/src/app/requirements.txt 4 | USER mike 5 | CMD python /usr/src/app/app.py -------------------------------------------------------------------------------- /integration/testdata/DS021/Dockerfile.allowed: -------------------------------------------------------------------------------- 1 | FROM node:12 2 | USER mike 3 | RUN apt-get -fmy install apt-utils && apt-get clean -------------------------------------------------------------------------------- /integration/testdata/DS021/Dockerfile.denied: -------------------------------------------------------------------------------- 1 | FROM node:12 2 | USER mike 3 | RUN apt-get install apt-utils && apt-get clean -------------------------------------------------------------------------------- /integration/testdata/DS022/Dockerfile.allowed: -------------------------------------------------------------------------------- 1 | FROM busybox:1.33.1 2 | USER mike -------------------------------------------------------------------------------- /integration/testdata/DS022/Dockerfile.denied: -------------------------------------------------------------------------------- 1 | FROM busybox:1.33.1 2 | USER mike 3 | MAINTAINER Lukas Martinelli -------------------------------------------------------------------------------- /integration/testdata/DS023/Dockerfile.allowed: -------------------------------------------------------------------------------- 1 | FROM busybox:1.33.1 2 | HEALTHCHECK CMD /bin/healthcheck 3 | 4 | FROM alpine:3.13 5 | HEALTHCHECK CMD /bin/healthcheck 6 | USER mike 7 | CMD ./app 8 | -------------------------------------------------------------------------------- /integration/testdata/DS023/Dockerfile.denied: -------------------------------------------------------------------------------- 1 | FROM busybox:1.33.1 2 | HEALTHCHECK CMD curl http://localhost:8080 3 | HEALTHCHECK CMD /bin/healthcheck 4 | 5 | FROM alpine:3.13 6 | HEALTHCHECK CMD /bin/healthcheck 7 | USER mike 8 | CMD ./app 9 | -------------------------------------------------------------------------------- /integration/testdata/DS024/Dockerfile.allowed: -------------------------------------------------------------------------------- 1 | FROM debian:9.13 2 | RUN apt-get update && apt-get install -y curl && apt-get clean 3 | USER mike 4 | CMD python /usr/src/app/app.py 5 | -------------------------------------------------------------------------------- /integration/testdata/DS024/Dockerfile.denied: -------------------------------------------------------------------------------- 1 | FROM debian:9.13 2 | RUN apt-get update && apt-get dist-upgrade && apt-get -y install curl && apt-get clean 3 | USER mike 4 | CMD python /usr/src/app/app.py 5 | -------------------------------------------------------------------------------- /kubernetes/lib/utils.rego: -------------------------------------------------------------------------------- 1 | package lib.utils 2 | 3 | has_key(x, k) { 4 | _ = x[k] 5 | } 6 | -------------------------------------------------------------------------------- /kubernetes/policies/advanced/capabilities_no_drop_at_least_one.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV004 2 | 3 | import data.lib.kubernetes 4 | import data.lib.utils 5 | 6 | default failCapsDropAny = false 7 | 8 | __rego_metadata__ := { 9 | "id": "KSV004", 10 | "avd_id": "AVD-KSV-0004", 11 | "title": "Unused capabilities should be dropped (drop any)", 12 | "short_code": "drop-unused-capabilities", 13 | "version": "v1.0.0", 14 | "severity": "LOW", 15 | "type": "Kubernetes Security Check", 16 | "description": "Security best practices require containers to run with minimal required capabilities.", 17 | "recommended_actions": "Specify at least one unneeded capability in 'containers[].securityContext.capabilities.drop'", 18 | "url": "https://kubesec.io/basics/containers-securitycontext-capabilities-drop-index-all/", 19 | } 20 | 21 | __rego_input__ := { 22 | "combine": false, 23 | "selector": [{"type": "kubernetes"}], 24 | } 25 | 26 | # getCapsDropAnyContainers returns names of all containers 27 | # which set securityContext.capabilities.drop 28 | getCapsDropAnyContainers[container] { 29 | allContainers := kubernetes.containers[_] 30 | utils.has_key(allContainers.securityContext.capabilities, "drop") 31 | container := allContainers.name 32 | } 33 | 34 | # getNoCapsDropContainers returns names of all containers which 35 | # do not set securityContext.capabilities.drop 36 | getNoCapsDropContainers[container] { 37 | container := kubernetes.containers[_].name 38 | not getCapsDropAnyContainers[container] 39 | } 40 | 41 | # failCapsDropAny is true if ANY container does not 42 | # set securityContext.capabilities.drop 43 | failCapsDropAny { 44 | count(getNoCapsDropContainers) > 0 45 | } 46 | 47 | deny[res] { 48 | failCapsDropAny 49 | 50 | msg := kubernetes.format(sprintf("Container '%s' of '%s' '%s' in '%s' namespace should set securityContext.capabilities.drop", [getNoCapsDropContainers[_], lower(kubernetes.kind), kubernetes.name, kubernetes.namespace])) 51 | 52 | res := { 53 | "msg": msg, 54 | "id": __rego_metadata__.id, 55 | "title": __rego_metadata__.title, 56 | "severity": __rego_metadata__.severity, 57 | "type": __rego_metadata__.type, 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /kubernetes/policies/advanced/manages_etc_hosts.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV007 2 | 3 | import data.lib.kubernetes 4 | import data.lib.utils 5 | 6 | default failHostAliases = false 7 | 8 | __rego_metadata__ := { 9 | "id": "KSV007", 10 | "avd_id": "AVD-KSV-0007", 11 | "title": "hostAliases is set", 12 | "short_code": "no-hostaliases", 13 | "version": "v1.0.0", 14 | "severity": "LOW", 15 | "type": "Kubernetes Security Check", 16 | "description": "Managing /etc/hosts aliases can prevent the container engine from modifying the file after a pod’s containers have already been started.", 17 | "recommended_actions": "Do not set 'spec.template.spec.hostAliases'.", 18 | } 19 | 20 | __rego_input__ := { 21 | "combine": false, 22 | "selector": [{"type": "kubernetes"}], 23 | } 24 | 25 | # failHostAliases is true if spec.hostAliases is set (on all controllers) 26 | failHostAliases { 27 | utils.has_key(kubernetes.host_aliases[_], "hostAliases") 28 | } 29 | 30 | deny[res] { 31 | failHostAliases 32 | 33 | msg := kubernetes.format(sprintf("'%s' '%s' in '%s' namespace should not set spec.template.spec.hostAliases", [lower(kubernetes.kind), kubernetes.name, kubernetes.namespace])) 34 | 35 | res := { 36 | "msg": msg, 37 | "id": __rego_metadata__.id, 38 | "title": __rego_metadata__.title, 39 | "severity": __rego_metadata__.severity, 40 | "type": __rego_metadata__.type, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /kubernetes/policies/advanced/protect_core_components_namespace.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV037 2 | 3 | import data.lib.kubernetes 4 | import data.lib.utils 5 | 6 | __rego_metadata__ := { 7 | "id": "KSV037", 8 | "avd_id": "AVD-KSV-0037", 9 | "title": "User Pods should not be placed in kube-system namespace", 10 | "short_code": "no-user-pods-in-system-namespace", 11 | "version": "v1.0.0", 12 | "severity": "MEDIUM", 13 | "type": "Kubernetes Security Check", 14 | "description": "ensure that User pods are not placed in kube-system namespace", 15 | "recommended_actions": "Deploy the use pods into a designated namespace which is not kube-system.", 16 | "url": "https://kubernetes.io/docs/reference/setup-tools/kubeadm/implementation-details/", 17 | } 18 | 19 | deny[res] { 20 | systemNamespaceInUse(input.metadata, input.spec) 21 | msg := sprintf("%s '%s' should not be set with 'kube-system' namespace", [kubernetes.kind, kubernetes.name]) 22 | res := { 23 | "msg": msg, 24 | "id": __rego_metadata__.id, 25 | "title": __rego_metadata__.title, 26 | "severity": __rego_metadata__.severity, 27 | "type": __rego_metadata__.type, 28 | } 29 | } 30 | 31 | systemNamespaceInUse(metadata, spec) { 32 | kubernetes.namespace == "kube-system" 33 | not core_component(metadata, spec) 34 | } 35 | 36 | core_component(metadata, spec) { 37 | kubernetes.has_field(metadata.labels, "tier") 38 | metadata.labels.tier == "control-plane" 39 | kubernetes.has_field(spec, "priorityClassName") 40 | spec.priorityClassName == "system-node-critical" 41 | kubernetes.has_field(metadata.labels, "component") 42 | coreComponentLabels := ["kube-apiserver", "etcd", "kube-controller-manager", "kube-scheduler"] 43 | metadata.labels.component = coreComponentLabels[_] 44 | } 45 | -------------------------------------------------------------------------------- /kubernetes/policies/advanced/protecting_pod_service_account_tokens.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV036 2 | 3 | import data.lib.kubernetes 4 | import data.lib.utils 5 | 6 | __rego_metadata__ := { 7 | "id": "KSV036", 8 | "avd_id": "AVD-KSV-0036", 9 | "title": "Protecting Pod service account tokens", 10 | "short_code": "no-auto-mount-service-token", 11 | "version": "v1.0.0", 12 | "severity": "MEDIUM", 13 | "type": "Kubernetes Security Check", 14 | "description": "ensure that Pod specifications disable the secret token being mounted by setting automountServiceAccountToken: false", 15 | "recommended_actions": "Remove 'container.apparmor.security.beta.kubernetes.io' annotation or set it to 'runtime/default'.", 16 | "url": "https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/#serviceaccount-admission-controller", 17 | } 18 | 19 | deny[res] { 20 | mountServiceAccountToken(input.spec) 21 | msg := kubernetes.format(sprintf("Container of %s '%s' should set 'spec.automountServiceAccountToken' to false", [kubernetes.kind, kubernetes.name])) 22 | 23 | res := { 24 | "msg": msg, 25 | "id": __rego_metadata__.id, 26 | "title": __rego_metadata__.title, 27 | "severity": __rego_metadata__.severity, 28 | "type": __rego_metadata__.type, 29 | } 30 | } 31 | 32 | mountServiceAccountToken(spec) { 33 | has_key(spec, "automountServiceAccountToken") 34 | spec.automountServiceAccountToken == true 35 | } 36 | 37 | # if there is no automountServiceAccountToken spec, check on volumeMount in containers. Service Account token is mounted on /var/run/secrets/kubernetes.io/serviceaccount 38 | mountServiceAccountToken(spec) { 39 | not has_key(spec, "automountServiceAccountToken") 40 | "/var/run/secrets/kubernetes.io/serviceaccount" == kubernetes.containers[_].volumeMounts[_].mountPath 41 | } 42 | 43 | has_key(x, k) { 44 | _ = x[k] 45 | } 46 | -------------------------------------------------------------------------------- /kubernetes/policies/advanced/protecting_pod_service_account_tokens_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV036 2 | 3 | test_protect_service_account_token_denied_with_automountServiceAccountToken { 4 | r := deny with input as { 5 | "kind": "pod", 6 | "name": "justPOod", 7 | "metadata": {"name": "nginx"}, 8 | "spec": { 9 | "automountServiceAccountToken": true, 10 | "containers": [{ 11 | "name": "nginx", 12 | "image": "nginx", 13 | "volumeMounts": [{ 14 | "name": "serviceaccount-vm", 15 | "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", 16 | }], 17 | }], 18 | }, 19 | } 20 | 21 | r[_].msg == "Container of pod 'nginx' should set 'spec.automountServiceAccountToken' to false" 22 | } 23 | 24 | test_protect_service_account_token_denied_without_automountServiceAccountToken { 25 | r := deny with input as { 26 | "kind": "pod", 27 | "name": "justPOod", 28 | "metadata": {"name": "nginx"}, 29 | "spec": {"containers": [{ 30 | "name": "nginx", 31 | "image": "nginx", 32 | "volumeMounts": [{ 33 | "name": "serviceaccount-vm", 34 | "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", 35 | }], 36 | }]}, 37 | } 38 | 39 | r[_].msg == "Container of pod 'nginx' should set 'spec.automountServiceAccountToken' to false" 40 | } 41 | 42 | test_protect_service_account_token_denied_without_mountPath { 43 | r := deny with input as { 44 | "kind": "pod", 45 | "name": "justPOod", 46 | "metadata": {"name": "nginx"}, 47 | "spec": {"containers": [{ 48 | "name": "nginx", 49 | "image": "nginx", 50 | "volumeMounts": [{"name": "serviceaccount-vm"}], 51 | }]}, 52 | } 53 | 54 | count(r) == 0 55 | } 56 | 57 | test_protect_service_account_token_allow { 58 | r := deny with input as { 59 | "kind": "pod", 60 | "name": "jusPOod", 61 | "metadata": {"name": "nginx"}, 62 | "spec": { 63 | "automountServiceAccountToken": false, 64 | "containers": [{ 65 | "name": "nginx", 66 | "image": "nginx", 67 | "volumeMounts": [{ 68 | "name": "serviceaccount-vm", 69 | "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", 70 | }], 71 | }], 72 | }, 73 | } 74 | 75 | count(r) == 0 76 | } 77 | -------------------------------------------------------------------------------- /kubernetes/policies/advanced/uses_untrusted_azure_registry.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV032 2 | 3 | import data.lib.kubernetes 4 | import data.lib.utils 5 | 6 | default failTrustedAzureRegistry = false 7 | 8 | __rego_metadata__ := { 9 | "id": "KSV032", 10 | "avd_id": "AVD-KSV-0032", 11 | "title": "All container images must start with the *.azurecr.io domain", 12 | "short_code": "use-azure-image-prefix", 13 | "version": "v1.0.0", 14 | "severity": "MEDIUM", 15 | "type": "Kubernetes Security Check", 16 | "description": "Containers should only use images from trusted registries.", 17 | "recommended_actions": "Use images from trusted Azure registries.", 18 | } 19 | 20 | __rego_input__ := { 21 | "combine": false, 22 | "selector": [{"type": "kubernetes"}], 23 | } 24 | 25 | # getContainersWithTrustedAzureRegistry returns a list of containers 26 | # with image from a trusted Azure registry 27 | getContainersWithTrustedAzureRegistry[name] { 28 | container := kubernetes.containers[_] 29 | image := container.image 30 | 31 | # get image registry/repo parts 32 | image_parts := split(image, "/") 33 | 34 | # images with only one part do not specify a registry 35 | count(image_parts) > 1 36 | registry = image_parts[0] 37 | endswith(registry, "azurecr.io") 38 | name := container.name 39 | } 40 | 41 | # getContainersWithUntrustedAzureRegistry returns a list of containers 42 | # with image from an untrusted Azure registry 43 | getContainersWithUntrustedAzureRegistry[name] { 44 | name := kubernetes.containers[_].name 45 | not getContainersWithTrustedAzureRegistry[name] 46 | } 47 | 48 | # failTrustedAzureRegistry is true if a container uses an image from an 49 | # untrusted Azure registry 50 | failTrustedAzureRegistry { 51 | count(getContainersWithUntrustedAzureRegistry) > 0 52 | } 53 | 54 | deny[res] { 55 | failTrustedAzureRegistry 56 | 57 | msg := kubernetes.format(sprintf("container %s of %s %s in %s namespace should restrict container image to your specific registry domain. For Azure any domain ending in 'azurecr.io'", [getContainersWithUntrustedAzureRegistry[_], lower(kubernetes.kind), kubernetes.name, kubernetes.namespace])) 58 | 59 | res := { 60 | "msg": msg, 61 | "id": __rego_metadata__.id, 62 | "title": __rego_metadata__.title, 63 | "severity": __rego_metadata__.severity, 64 | "type": __rego_metadata__.type, 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /kubernetes/policies/advanced/uses_untrusted_gcr_registry.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV033 2 | 3 | import data.lib.kubernetes 4 | import data.lib.utils 5 | 6 | default failTrustedGCRRegistry = false 7 | 8 | __rego_metadata__ := { 9 | "id": "KSV033", 10 | "avd_id": "AVD-KSV-0033", 11 | "title": "All container images must start with a GCR domain", 12 | "short_code": "use-gcr-domain", 13 | "version": "v1.0.0", 14 | "severity": "MEDIUM", 15 | "type": "Kubernetes Security Check", 16 | "description": "Containers should only use images from trusted GCR registries.", 17 | "recommended_actions": "Use images from trusted GCR registries.", 18 | } 19 | 20 | __rego_input__ := { 21 | "combine": false, 22 | "selector": [{"type": "kubernetes"}], 23 | } 24 | 25 | # list of trusted GCR registries 26 | trusted_gcr_registries = [ 27 | "gcr.io", 28 | "us.gcr.io", 29 | "eu.gcr.io", 30 | "asia.gcr.io", 31 | ] 32 | 33 | # getContainersWithTrustedGCRRegistry returns a list of containers 34 | # with image from a trusted gcr registry 35 | getContainersWithTrustedGCRRegistry[name] { 36 | container := kubernetes.containers[_] 37 | image := container.image 38 | 39 | # get image registry/repo parts 40 | image_parts := split(image, "/") 41 | 42 | # images with only one part do not specify a registry 43 | count(image_parts) > 1 44 | registry = image_parts[0] 45 | trusted := trusted_gcr_registries[_] 46 | endswith(registry, trusted) 47 | name := container.name 48 | } 49 | 50 | # getContainersWithUntrustedGCRRegistry returns a list of containers 51 | # with image from an untrusted gcr registry 52 | getContainersWithUntrustedGCRRegistry[name] { 53 | name := kubernetes.containers[_].name 54 | not getContainersWithTrustedGCRRegistry[name] 55 | } 56 | 57 | # failTrustedGCRRegistry is true if a container uses an image from an 58 | # untrusted gcr registry 59 | failTrustedGCRRegistry { 60 | count(getContainersWithUntrustedGCRRegistry) > 0 61 | } 62 | 63 | deny[res] { 64 | failTrustedGCRRegistry 65 | 66 | msg := kubernetes.format(sprintf("container %s of %s %s in %s namespace should restrict container image to your specific registry domain. See the full GCR list here: https://cloud.google.com/container-registry/docs/overview#registries", [getContainersWithUntrustedGCRRegistry[_], lower(kubernetes.kind), kubernetes.name, kubernetes.namespace])) 67 | 68 | res := { 69 | "msg": msg, 70 | "id": __rego_metadata__.id, 71 | "title": __rego_metadata__.title, 72 | "severity": __rego_metadata__.severity, 73 | "type": __rego_metadata__.type, 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /kubernetes/policies/advanced/uses_untrusted_public_registries.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV034 2 | 3 | import data.lib.kubernetes 4 | import data.lib.utils 5 | 6 | default failPublicRegistry = false 7 | 8 | __rego_metadata__ := { 9 | "id": "KSV034", 10 | "avd_id": "AVD-KSV-0034", 11 | "title": "Container images from public registries used", 12 | "short_code": "no-public-registries", 13 | "version": "v1.0.0", 14 | "severity": "MEDIUM", 15 | "type": "Kubernetes Security Check", 16 | "description": "Container images must not start with an empty prefix or a defined public registry domain.", 17 | "recommended_actions": "Use images from private registries.", 18 | } 19 | 20 | __rego_input__ := { 21 | "combine": false, 22 | "selector": [{"type": "kubernetes"}], 23 | } 24 | 25 | # list of untrusted public registries 26 | untrusted_public_registries = [ 27 | "docker.io", 28 | "ghcr.io", 29 | ] 30 | 31 | # getContainersWithPublicRegistries returns a list of containers 32 | # with public registry prefixes 33 | getContainersWithPublicRegistries[name] { 34 | container := kubernetes.containers[_] 35 | image := container.image 36 | untrusted := untrusted_public_registries[_] 37 | startswith(image, untrusted) 38 | name := container.name 39 | } 40 | 41 | # getContainersWithPublicRegistries returns a list of containers 42 | # with image without registry prefix 43 | getContainersWithPublicRegistries[name] { 44 | container := kubernetes.containers[_] 45 | image := container.image 46 | image_parts := split(image, "/") # get image registry/repo parts 47 | count(image_parts) > 0 48 | not contains(image_parts[0], ".") # check if first part is a url (assuming we have "." in url) 49 | name := container.name 50 | } 51 | 52 | # failPublicRegistry is true if a container uses an image from an 53 | # untrusted public registry 54 | failPublicRegistry { 55 | count(getContainersWithPublicRegistries) > 0 56 | } 57 | 58 | deny[res] { 59 | failPublicRegistry 60 | 61 | msg := kubernetes.format(sprintf("Container '%s' of %s '%s' should restrict container image to use private registries", [getContainersWithPublicRegistries[_], kubernetes.kind, kubernetes.name])) 62 | 63 | res := { 64 | "msg": msg, 65 | "id": __rego_metadata__.id, 66 | "title": __rego_metadata__.title, 67 | "severity": __rego_metadata__.severity, 68 | "type": __rego_metadata__.type, 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /kubernetes/policies/general/CPU_not_limited.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV011 2 | 3 | import data.lib.kubernetes 4 | import data.lib.utils 5 | 6 | default failLimitsCPU = false 7 | 8 | __rego_metadata__ := { 9 | "id": "KSV011", 10 | "avd_id": "AVD-KSV-0011", 11 | "title": "CPU not limited", 12 | "short_code": "limit-cpu", 13 | "version": "v1.0.0", 14 | "severity": "LOW", 15 | "type": "Kubernetes Security Check", 16 | "description": "Enforcing CPU limits prevents DoS via resource exhaustion.", 17 | "recommended_actions": "Set a limit value under 'containers[].resources.limits.cpu'.", 18 | "url": "https://cloud.google.com/blog/products/containers-kubernetes/kubernetes-best-practices-resource-requests-and-limits", 19 | } 20 | 21 | __rego_input__ := { 22 | "combine": false, 23 | "selector": [{"type": "kubernetes"}], 24 | } 25 | 26 | # getLimitsCPUContainers returns all containers which have set resources.limits.cpu 27 | getLimitsCPUContainers[container] { 28 | allContainers := kubernetes.containers[_] 29 | utils.has_key(allContainers.resources.limits, "cpu") 30 | container := allContainers.name 31 | } 32 | 33 | # getNoLimitsCPUContainers returns all containers which have not set 34 | # resources.limits.cpu 35 | getNoLimitsCPUContainers[container] { 36 | container := kubernetes.containers[_].name 37 | not getLimitsCPUContainers[container] 38 | } 39 | 40 | # failLimitsCPU is true if containers[].resources.limits.cpu is not set 41 | # for ANY container 42 | failLimitsCPU { 43 | count(getNoLimitsCPUContainers) > 0 44 | } 45 | 46 | deny[res] { 47 | failLimitsCPU 48 | 49 | msg := kubernetes.format(sprintf("Container '%s' of %s '%s' should set 'resources.limits.cpu'", [getNoLimitsCPUContainers[_], kubernetes.kind, kubernetes.name])) 50 | 51 | res := { 52 | "msg": msg, 53 | "id": __rego_metadata__.id, 54 | "title": __rego_metadata__.title, 55 | "severity": __rego_metadata__.severity, 56 | "type": __rego_metadata__.type, 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /kubernetes/policies/general/CPU_not_limited_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV011 2 | 3 | test_CPU_not_limited_denied { 4 | r := deny with input as { 5 | "apiVersion": "v1", 6 | "kind": "Pod", 7 | "metadata": {"name": "hello-cpu-limit"}, 8 | "spec": {"containers": [{ 9 | "command": [ 10 | "sh", 11 | "-c", 12 | "echo 'Hello' && sleep 1h", 13 | ], 14 | "image": "busybox", 15 | "name": "hello", 16 | }]}, 17 | } 18 | 19 | count(r) == 1 20 | r[_].msg == "Container 'hello' of Pod 'hello-cpu-limit' should set 'resources.limits.cpu'" 21 | } 22 | 23 | test_CPU_limited_allowed { 24 | r := deny with input as { 25 | "apiVersion": "v1", 26 | "kind": "Pod", 27 | "metadata": {"name": "hello-cpu-limit"}, 28 | "spec": {"containers": [{ 29 | "command": [ 30 | "sh", 31 | "-c", 32 | "echo 'Hello' && sleep 1h", 33 | ], 34 | "image": "busybox", 35 | "name": "hello", 36 | "resources": {"limits": {"cpu": "500m"}}, 37 | }]}, 38 | } 39 | 40 | count(r) == 0 41 | } 42 | -------------------------------------------------------------------------------- /kubernetes/policies/general/CPU_requests_not_specified.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV015 2 | 3 | import data.lib.kubernetes 4 | import data.lib.utils 5 | 6 | default failRequestsCPU = false 7 | 8 | __rego_metadata__ := { 9 | "id": "KSV015", 10 | "avd_id": "AVD-KSV-0015", 11 | "title": "CPU requests not specified", 12 | "short_code": "no-unspecified-cpu-requests", 13 | "version": "v1.0.0", 14 | "severity": "LOW", 15 | "type": "Kubernetes Security Check", 16 | "description": "When containers have resource requests specified, the scheduler can make better decisions about which nodes to place pods on, and how to deal with resource contention.", 17 | "recommended_actions": "Set 'containers[].resources.requests.cpu'.", 18 | "url": "https://cloud.google.com/blog/products/containers-kubernetes/kubernetes-best-practices-resource-requests-and-limits", 19 | } 20 | 21 | __rego_input__ := { 22 | "combine": false, 23 | "selector": [{"type": "kubernetes"}], 24 | } 25 | 26 | # getRequestsCPUContainers returns all containers which have set resources.requests.cpu 27 | getRequestsCPUContainers[container] { 28 | allContainers := kubernetes.containers[_] 29 | utils.has_key(allContainers.resources.requests, "cpu") 30 | container := allContainers.name 31 | } 32 | 33 | # getNoRequestsCPUContainers returns all containers which have not set 34 | # resources.requests.cpu 35 | getNoRequestsCPUContainers[container] { 36 | container := kubernetes.containers[_].name 37 | not getRequestsCPUContainers[container] 38 | } 39 | 40 | # failRequestsCPU is true if containers[].resources.requests.cpu is not set 41 | # for ANY container 42 | failRequestsCPU { 43 | count(getNoRequestsCPUContainers) > 0 44 | } 45 | 46 | deny[res] { 47 | failRequestsCPU 48 | 49 | msg := kubernetes.format(sprintf("Container '%s' of %s '%s' should set 'resources.requests.cpu'", [getNoRequestsCPUContainers[_], kubernetes.kind, kubernetes.name])) 50 | 51 | res := { 52 | "msg": msg, 53 | "id": __rego_metadata__.id, 54 | "title": __rego_metadata__.title, 55 | "severity": __rego_metadata__.severity, 56 | "type": __rego_metadata__.type, 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /kubernetes/policies/general/CPU_requests_not_specified_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV015 2 | 3 | test_CPU_requests_not_specified_denied { 4 | r := deny with input as { 5 | "apiVersion": "v1", 6 | "kind": "Pod", 7 | "metadata": {"name": "hello-cpu-request"}, 8 | "spec": {"containers": [{ 9 | "command": [ 10 | "sh", 11 | "-c", 12 | "echo 'Hello' && sleep 1h", 13 | ], 14 | "image": "busybox", 15 | "name": "hello", 16 | }]}, 17 | } 18 | 19 | count(r) == 1 20 | r[_].msg == "Container 'hello' of Pod 'hello-cpu-request' should set 'resources.requests.cpu'" 21 | } 22 | 23 | test_CPU_requests_specified_allowed { 24 | r := deny with input as { 25 | "apiVersion": "v1", 26 | "kind": "Pod", 27 | "metadata": {"name": "hello-cpu-limit"}, 28 | "spec": {"containers": [{ 29 | "command": [ 30 | "sh", 31 | "-c", 32 | "echo 'Hello' && sleep 1h", 33 | ], 34 | "image": "busybox", 35 | "name": "hello", 36 | "resources": {"requests": {"cpu": "250m"}}, 37 | }]}, 38 | } 39 | 40 | count(r) == 0 41 | } 42 | -------------------------------------------------------------------------------- /kubernetes/policies/general/SYS_ADMIN_capability.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV005 2 | 3 | import data.lib.kubernetes 4 | 5 | default failCapsSysAdmin = false 6 | 7 | __rego_metadata__ := { 8 | "id": "KSV005", 9 | "avd_id": "AVD-KSV-0005", 10 | "title": "SYS_ADMIN capability added", 11 | "short_code": "no-sysadmin-capability", 12 | "version": "v1.0.0", 13 | "severity": "HIGH", 14 | "type": "Kubernetes Security Check", 15 | "description": "SYS_ADMIN gives the processes running inside the container privileges that are equivalent to root.", 16 | "recommended_actions": "Remove the SYS_ADMIN capability from 'containers[].securityContext.capabilities.add'.", 17 | "url": "https://kubesec.io/basics/containers-securitycontext-capabilities-add-index-sys-admin/", 18 | } 19 | 20 | __rego_input__ := { 21 | "combine": false, 22 | "selector": [{"type": "kubernetes"}], 23 | } 24 | 25 | # getCapsSysAdmin returns the names of all containers which include 26 | # 'SYS_ADMIN' in securityContext.capabilities.add. 27 | getCapsSysAdmin[container] { 28 | allContainers := kubernetes.containers[_] 29 | allContainers.securityContext.capabilities.add[_] == "SYS_ADMIN" 30 | container := allContainers.name 31 | } 32 | 33 | # failCapsSysAdmin is true if securityContext.capabilities.add 34 | # includes 'SYS_ADMIN'. 35 | failCapsSysAdmin { 36 | count(getCapsSysAdmin) > 0 37 | } 38 | 39 | deny[res] { 40 | failCapsSysAdmin 41 | 42 | msg := kubernetes.format(sprintf("Container '%s' of %s '%s' should not include 'SYS_ADMIN' in 'securityContext.capabilities.add'", [getCapsSysAdmin[_], kubernetes.kind, kubernetes.name])) 43 | 44 | res := { 45 | "msg": msg, 46 | "id": __rego_metadata__.id, 47 | "title": __rego_metadata__.title, 48 | "severity": __rego_metadata__.severity, 49 | "type": __rego_metadata__.type, 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /kubernetes/policies/general/SYS_ADMIN_capability_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV005 2 | 3 | test_cap_without_sys_admin_allowed { 4 | r := deny with input as { 5 | "apiVersion": "v1", 6 | "kind": "Pod", 7 | "metadata": {"name": "hello-sys-admin-capabilities"}, 8 | "spec": {"containers": [{ 9 | "command": [ 10 | "sh", 11 | "-c", 12 | "echo 'Hello' && sleep 1h", 13 | ], 14 | "image": "busybox", 15 | "name": "hello", 16 | }]}, 17 | } 18 | 19 | count(r) == 0 20 | } 21 | 22 | test_cap_add_sys_admin_denied { 23 | r := deny with input as { 24 | "apiVersion": "v1", 25 | "kind": "Pod", 26 | "metadata": {"name": "hello-sys-admin-capabilities"}, 27 | "spec": {"containers": [{ 28 | "command": [ 29 | "sh", 30 | "-c", 31 | "echo 'Hello' && sleep 1h", 32 | ], 33 | "image": "busybox", 34 | "name": "hello", 35 | "securityContext": {"capabilities": {"add": ["SYS_ADMIN"]}}, 36 | }]}, 37 | } 38 | 39 | count(r) == 1 40 | r[_].msg == "Container 'hello' of Pod 'hello-sys-admin-capabilities' should not include 'SYS_ADMIN' in 'securityContext.capabilities.add'" 41 | } 42 | -------------------------------------------------------------------------------- /kubernetes/policies/general/capabilities_no_drop_all.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV003 2 | 3 | import data.lib.kubernetes 4 | 5 | default checkCapsDropAll = false 6 | 7 | __rego_metadata__ := { 8 | "id": "KSV003", 9 | "avd_id": "AVD-KSV-0003", 10 | "title": "Default capabilities not dropped", 11 | "short_code": "drop-default-capabilities", 12 | "version": "v1.0.0", 13 | "severity": "LOW", 14 | "type": "Kubernetes Security Check", 15 | "description": "The container should drop all default capabilities and add only those that are needed for its execution.", 16 | "recommended_actions": "Add 'ALL' to containers[].securityContext.capabilities.drop.", 17 | "url": "https://kubesec.io/basics/containers-securitycontext-capabilities-drop-index-all/", 18 | } 19 | 20 | __rego_input__ := { 21 | "combine": false, 22 | "selector": [{"type": "kubernetes"}], 23 | } 24 | 25 | # Get all containers which include 'ALL' in security.capabilities.drop 26 | getCapsDropAllContainers[container] { 27 | allContainers := kubernetes.containers[_] 28 | allContainers.securityContext.capabilities.drop[_] == "ALL" 29 | container := allContainers.name 30 | } 31 | 32 | # Get all containers which don't include 'ALL' in security.capabilities.drop 33 | getCapsNoDropAllContainers[container] { 34 | container := kubernetes.containers[_].name 35 | not getCapsDropAllContainers[container] 36 | } 37 | 38 | # checkCapsDropAll is true if capabilities drop does not include 'ALL', 39 | # or if capabilities drop is not specified at all. 40 | checkCapsDropAll { 41 | count(getCapsNoDropAllContainers) > 0 42 | } 43 | 44 | deny[res] { 45 | checkCapsDropAll 46 | 47 | msg := kubernetes.format(sprintf("Container '%s' of %s '%s' should add 'ALL' to 'securityContext.capabilities.drop'", [getCapsNoDropAllContainers[_], kubernetes.kind, kubernetes.name])) 48 | 49 | res := { 50 | "msg": msg, 51 | "id": __rego_metadata__.id, 52 | "title": __rego_metadata__.title, 53 | "severity": __rego_metadata__.severity, 54 | "type": __rego_metadata__.type, 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /kubernetes/policies/general/capabilities_no_drop_all_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV003 2 | 3 | test_cap_no_drop_all_denied { 4 | r := deny with input as { 5 | "apiVersion": "v1", 6 | "kind": "Pod", 7 | "metadata": {"name": "hello-drop-capabilities"}, 8 | "spec": {"containers": [{ 9 | "command": [ 10 | "sh", 11 | "-c", 12 | "echo 'Hello' && sleep 1h", 13 | ], 14 | "image": "busybox", 15 | "name": "hello", 16 | }]}, 17 | } 18 | 19 | count(r) == 1 20 | r[_].msg == "Container 'hello' of Pod 'hello-drop-capabilities' should add 'ALL' to 'securityContext.capabilities.drop'" 21 | } 22 | 23 | test_cap_drop_all_allowed { 24 | r := deny with input as { 25 | "apiVersion": "v1", 26 | "kind": "Pod", 27 | "metadata": {"name": "hello-drop-capabilities"}, 28 | "spec": {"containers": [{ 29 | "command": [ 30 | "sh", 31 | "-c", 32 | "echo 'Hello' && sleep 1h", 33 | ], 34 | "image": "busybox", 35 | "name": "hello", 36 | "securityContext": {"capabilities": {"drop": ["ALL"]}}, 37 | }]}, 38 | } 39 | 40 | count(r) == 0 41 | } 42 | -------------------------------------------------------------------------------- /kubernetes/policies/general/deprecated_api_version.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV101 2 | 3 | import data.lib.kubernetes 4 | import data.lib.utils 5 | 6 | __rego_metadata__ := { 7 | "id": "KSV101", 8 | "title": "Resource is using a deprecated API version", 9 | "version": "v1.0.0", 10 | "severity": "LOW", 11 | "type": "Kubernetes Security Check", 12 | "description": "Check if any objects are using a deprecated version of API.", 13 | "recommended_actions": "Update to use a newer API version.", 14 | "url": "https://kubernetes.io/docs/reference/using-api/deprecation-guide/", 15 | } 16 | 17 | __rego_input__ := { 18 | "combine": false, 19 | "selector": [{"type": "kubernetes"}], 20 | } 21 | 22 | recommendedVersions := { 23 | "extensions/v1beta1": { 24 | "Deployment": "apps/v1", 25 | "DaemonSet": "apps/v1", 26 | "Ingress": "apps/v1", 27 | "NetworkPolicy": "networking.k8s.io/v1", 28 | "ReplicaSet": "apps/v1", 29 | "PodSecurityPolicy": "policy/v1beta1", 30 | }, 31 | "apps/v1beta1": { 32 | "Deployment": "apps/v1", 33 | "StatefulSet": "apps/v1", 34 | "ReplicaSet": "apps/v1", 35 | }, 36 | "apps/v1beta2": { 37 | "Deployment": "apps/v1", 38 | "DaemonSet": "apps/v1", 39 | "StatefulSet": "apps/v1", 40 | "ReplicaSet": "apps/v1", 41 | }, 42 | } 43 | 44 | deny[res] { 45 | version := recommendedVersions[kubernetes.apiVersion][kubernetes.kind] 46 | msg := kubernetes.format(sprintf("%s is using deprecated 'apiVersion: %s', it should be 'apiVersion: %s'", [lower(kubernetes.name), kubernetes.apiVersion, version])) 47 | 48 | res := { 49 | "msg": msg, 50 | "id": __rego_metadata__.id, 51 | "title": __rego_metadata__.title, 52 | "severity": __rego_metadata__.severity, 53 | "type": __rego_metadata__.type, 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /kubernetes/policies/general/deprecated_api_version_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV101 2 | 3 | # Check for apiVersion is valid 4 | test_apiIsUpToDate { 5 | res := deny with input as { 6 | "apiVersion": "apps/v1", 7 | "kind": "Deployment", 8 | "metadata": {"name": "mongo-deployment"}, 9 | "spec": {"template": {"spec": { 10 | "containers": [{ 11 | "name": "carts-db", 12 | "image": "mongo", 13 | "securityContext": { 14 | "runAsNonRoot": true, 15 | "allowPrivilegeEscalation": true, 16 | }, 17 | }], 18 | "initContainers": [{ 19 | "name": "init-svc", 20 | "image": "busybox:1.28", 21 | "securityContext": {"allowPrivilegeEscalation": false}, 22 | }], 23 | }}}, 24 | } 25 | 26 | count(res) == 0 27 | } 28 | 29 | # Check for old deprecated apiVersion X kind Deployment 30 | test_apiIsDeprecated { 31 | res := deny with input as { 32 | "apiVersion": "apps/v1beta2", 33 | "kind": "Deployment", 34 | "metadata": {"name": "mongo-deployment"}, 35 | "spec": {"template": {"spec": { 36 | "containers": [{ 37 | "name": "carts-db", 38 | "image": "mongo", 39 | "securityContext": { 40 | "runAsNonRoot": true, 41 | "allowPrivilegeEscalation": true, 42 | }, 43 | }], 44 | "initContainers": [{ 45 | "name": "init-svc", 46 | "image": "busybox:1.28", 47 | "securityContext": {"allowPrivilegeEscalation": false}, 48 | }], 49 | }}}, 50 | } 51 | 52 | res[_].msg == "mongo-deployment is using deprecated 'apiVersion: apps/v1beta2', it should be 'apiVersion: apps/v1'" 53 | } 54 | 55 | # Check for old deprecated apiVersion X kind NetworkPolicy 56 | test_apiIsDeprecatedNetwork { 57 | res := deny with input as { 58 | "apiVersion": "extensions/v1beta1", 59 | "kind": "NetworkPolicy", 60 | "metadata": {"name": "web-allow-external"}, 61 | "spec": {"template": {"spec": {"podSelector:": [{"matchLabels": {"app": "web"}}]}}}, 62 | } 63 | 64 | res[_].msg == "web-allow-external is using deprecated 'apiVersion: extensions/v1beta1', it should be 'apiVersion: networking.k8s.io/v1'" 65 | } 66 | -------------------------------------------------------------------------------- /kubernetes/policies/general/file_system_not_read_only.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV014 2 | 3 | import data.lib.kubernetes 4 | 5 | default failReadOnlyRootFilesystem = false 6 | 7 | __rego_metadata__ := { 8 | "id": "KSV014", 9 | "avd_id": "AVD-KSV-0014", 10 | "title": "Root file system is not read-only", 11 | "short_code": "use-readonly-filesystem", 12 | "version": "v1.0.0", 13 | "severity": "LOW", 14 | "type": "Kubernetes Security Check", 15 | "description": "An immutable root file system prevents applications from writing to their local disk. This can limit intrusions, as attackers will not be able to tamper with the file system or write foreign executables to disk.", 16 | "recommended_actions": "Change 'containers[].securityContext.readOnlyRootFilesystem' to 'true'.", 17 | "url": "https://kubesec.io/basics/containers-securitycontext-readonlyrootfilesystem-true/", 18 | } 19 | 20 | __rego_input__ := { 21 | "combine": false, 22 | "selector": [{"type": "kubernetes"}], 23 | } 24 | 25 | # getReadOnlyRootFilesystemContainers returns all containers that have 26 | # securityContext.readOnlyFilesystem set to true. 27 | getReadOnlyRootFilesystemContainers[container] { 28 | allContainers := kubernetes.containers[_] 29 | allContainers.securityContext.readOnlyRootFilesystem == true 30 | container := allContainers.name 31 | } 32 | 33 | # getNotReadOnlyRootFilesystemContainers returns all containers that have 34 | # securityContext.readOnlyRootFilesystem set to false or not set at all. 35 | getNotReadOnlyRootFilesystemContainers[container] { 36 | container := kubernetes.containers[_].name 37 | not getReadOnlyRootFilesystemContainers[container] 38 | } 39 | 40 | # failReadOnlyRootFilesystem is true if ANY container sets 41 | # securityContext.readOnlyRootFilesystem set to false or not set at all. 42 | failReadOnlyRootFilesystem { 43 | count(getNotReadOnlyRootFilesystemContainers) > 0 44 | } 45 | 46 | deny[res] { 47 | failReadOnlyRootFilesystem 48 | 49 | msg := kubernetes.format(sprintf("Container '%s' of %s '%s' should set 'securityContext.readOnlyRootFilesystem' to true", [getNotReadOnlyRootFilesystemContainers[_], kubernetes.kind, kubernetes.name])) 50 | 51 | res := { 52 | "msg": msg, 53 | "id": __rego_metadata__.id, 54 | "title": __rego_metadata__.title, 55 | "severity": __rego_metadata__.severity, 56 | "type": __rego_metadata__.type, 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /kubernetes/policies/general/file_system_not_read_only_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV014 2 | 3 | test_read_only_root_file_system_not_set_denied { 4 | r := deny with input as { 5 | "apiVersion": "v1", 6 | "kind": "Pod", 7 | "metadata": {"name": "hello-fs-not-readonly"}, 8 | "spec": {"containers": [{ 9 | "command": [ 10 | "sh", 11 | "-c", 12 | "echo 'Hello' && sleep 1h", 13 | ], 14 | "image": "busybox", 15 | "name": "hello", 16 | }]}, 17 | } 18 | 19 | count(r) == 1 20 | r[_].msg == "Container 'hello' of Pod 'hello-fs-not-readonly' should set 'securityContext.readOnlyRootFilesystem' to true" 21 | } 22 | 23 | test_read_only_root_file_system_false_denied { 24 | r := deny with input as { 25 | "apiVersion": "v1", 26 | "kind": "Pod", 27 | "metadata": {"name": "hello-fs-not-readonly"}, 28 | "spec": {"containers": [{ 29 | "command": [ 30 | "sh", 31 | "-c", 32 | "echo 'Hello' && sleep 1h", 33 | ], 34 | "image": "busybox", 35 | "name": "hello", 36 | "securityContext": {"readOnlyRootFilesystem": false}, 37 | }]}, 38 | } 39 | 40 | count(r) == 1 41 | r[_].msg == "Container 'hello' of Pod 'hello-fs-not-readonly' should set 'securityContext.readOnlyRootFilesystem' to true" 42 | } 43 | 44 | test_read_only_root_file_system_true_allowed { 45 | r := deny with input as { 46 | "apiVersion": "v1", 47 | "kind": "Pod", 48 | "metadata": {"name": "hello-fs-not-readonly"}, 49 | "spec": {"containers": [{ 50 | "command": [ 51 | "sh", 52 | "-c", 53 | "echo 'Hello' && sleep 1h", 54 | ], 55 | "image": "busybox", 56 | "name": "hello", 57 | "securityContext": {"readOnlyRootFilesystem": true}, 58 | }]}, 59 | } 60 | 61 | count(r) == 0 62 | } 63 | -------------------------------------------------------------------------------- /kubernetes/policies/general/memory_not_limited.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV018 2 | 3 | import data.lib.kubernetes 4 | import data.lib.utils 5 | 6 | default failLimitsMemory = false 7 | 8 | __rego_metadata__ := { 9 | "id": "KSV018", 10 | "avd_id": "AVD-KSV-0018", 11 | "title": "Memory not limited", 12 | "short_code": "limit-memory", 13 | "version": "v1.0.0", 14 | "severity": "LOW", 15 | "type": "Kubernetes Security Check", 16 | "description": "Enforcing memory limits prevents DoS via resource exhaustion.", 17 | "recommended_actions": "Set a limit value under 'containers[].resources.limits.memory'.", 18 | "url": "https://kubesec.io/basics/containers-resources-limits-memory/", 19 | } 20 | 21 | __rego_input__ := { 22 | "combine": false, 23 | "selector": [{"type": "kubernetes"}], 24 | } 25 | 26 | # getLimitsMemoryContainers returns all containers which have set resources.limits.memory 27 | getLimitsMemoryContainers[container] { 28 | allContainers := kubernetes.containers[_] 29 | utils.has_key(allContainers.resources.limits, "memory") 30 | container := allContainers.name 31 | } 32 | 33 | # getNoLimitsMemoryContainers returns all containers which have not set 34 | # resources.limits.memory 35 | getNoLimitsMemoryContainers[container] { 36 | container := kubernetes.containers[_].name 37 | not getLimitsMemoryContainers[container] 38 | } 39 | 40 | # failLimitsMemory is true if containers[].resources.limits.memory is not set 41 | # for ANY container 42 | failLimitsMemory { 43 | count(getNoLimitsMemoryContainers) > 0 44 | } 45 | 46 | deny[res] { 47 | failLimitsMemory 48 | 49 | msg := kubernetes.format(sprintf("Container '%s' of %s '%s' should set 'resources.limits.memory'", [getNoLimitsMemoryContainers[_], kubernetes.kind, kubernetes.name])) 50 | res := { 51 | "msg": msg, 52 | "id": __rego_metadata__.id, 53 | "title": __rego_metadata__.title, 54 | "severity": __rego_metadata__.severity, 55 | "type": __rego_metadata__.type, 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /kubernetes/policies/general/memory_not_limited_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV018 2 | 3 | test_memory_not_limited_denied { 4 | r := deny with input as { 5 | "apiVersion": "v1", 6 | "kind": "Pod", 7 | "metadata": {"name": "hello-memory-limit"}, 8 | "spec": {"containers": [{ 9 | "command": [ 10 | "sh", 11 | "-c", 12 | "echo 'Hello' && sleep 1h", 13 | ], 14 | "image": "busybox", 15 | "name": "hello", 16 | }]}, 17 | } 18 | 19 | count(r) == 1 20 | r[_].msg == "Container 'hello' of Pod 'hello-memory-limit' should set 'resources.limits.memory'" 21 | } 22 | 23 | test_memory_limited_allowed { 24 | r := deny with input as { 25 | "apiVersion": "v1", 26 | "kind": "Pod", 27 | "metadata": {"name": "hello-cpu-limit"}, 28 | "spec": {"containers": [{ 29 | "command": [ 30 | "sh", 31 | "-c", 32 | "echo 'Hello' && sleep 1h", 33 | ], 34 | "image": "busybox", 35 | "name": "hello", 36 | "resources": {"limits": {"memory": "128Mi"}}, 37 | }]}, 38 | } 39 | 40 | count(r) == 0 41 | } 42 | -------------------------------------------------------------------------------- /kubernetes/policies/general/memory_requests_not_specified.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV016 2 | 3 | import data.lib.kubernetes 4 | import data.lib.utils 5 | 6 | default failRequestsMemory = false 7 | 8 | __rego_metadata__ := { 9 | "id": "KSV016", 10 | "avd_id": "AVD-KSV-0016", 11 | "title": "Memory requests not specified", 12 | "short_code": "no-unspecified-memory-requests", 13 | "version": "v1.0.0", 14 | "severity": "LOW", 15 | "type": "Kubernetes Security Check", 16 | "description": "When containers have memory requests specified, the scheduler can make better decisions about which nodes to place pods on, and how to deal with resource contention.", 17 | "recommended_actions": "Set 'containers[].resources.requests.memory'.", 18 | "url": "https://kubesec.io/basics/containers-resources-limits-memory/", 19 | } 20 | 21 | __rego_input__ := { 22 | "combine": false, 23 | "selector": [{"type": "kubernetes"}], 24 | } 25 | 26 | # getRequestsMemoryContainers returns all containers which have set resources.requests.memory 27 | getRequestsMemoryContainers[container] { 28 | allContainers := kubernetes.containers[_] 29 | utils.has_key(allContainers.resources.requests, "memory") 30 | container := allContainers.name 31 | } 32 | 33 | # getNoRequestsMemoryContainers returns all containers which have not set 34 | # resources.requests.memory 35 | getNoRequestsMemoryContainers[container] { 36 | container := kubernetes.containers[_].name 37 | not getRequestsMemoryContainers[container] 38 | } 39 | 40 | # failRequestsMemory is true if containers[].resources.requests.memory is not set 41 | # for ANY container 42 | failRequestsMemory { 43 | count(getNoRequestsMemoryContainers) > 0 44 | } 45 | 46 | deny[res] { 47 | failRequestsMemory 48 | 49 | msg := kubernetes.format(sprintf("Container '%s' of %s '%s' should set 'resources.requests.memory'", [getNoRequestsMemoryContainers[_], kubernetes.kind, kubernetes.name])) 50 | 51 | res := { 52 | "msg": msg, 53 | "id": __rego_metadata__.id, 54 | "title": __rego_metadata__.title, 55 | "severity": __rego_metadata__.severity, 56 | "type": __rego_metadata__.type, 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /kubernetes/policies/general/memory_requests_not_specified_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV016 2 | 3 | test_memory_requests_not_specified_denied { 4 | r := deny with input as { 5 | "apiVersion": "v1", 6 | "kind": "Pod", 7 | "metadata": {"name": "hello-memory-requests"}, 8 | "spec": {"containers": [{ 9 | "command": [ 10 | "sh", 11 | "-c", 12 | "echo 'Hello' && sleep 1h", 13 | ], 14 | "image": "busybox", 15 | "name": "hello", 16 | }]}, 17 | } 18 | 19 | count(r) == 1 20 | r[_].msg == "Container 'hello' of Pod 'hello-memory-requests' should set 'resources.requests.memory'" 21 | } 22 | 23 | test_memory_requests_specified_allowed { 24 | r := deny with input as { 25 | "apiVersion": "v1", 26 | "kind": "Pod", 27 | "metadata": {"name": "hello-cpu-limit"}, 28 | "spec": {"containers": [{ 29 | "command": [ 30 | "sh", 31 | "-c", 32 | "echo 'Hello' && sleep 1h", 33 | ], 34 | "image": "busybox", 35 | "name": "hello", 36 | "resources": {"requests": {"memory": "64Mi"}}, 37 | }]}, 38 | } 39 | 40 | count(r) == 0 41 | } 42 | -------------------------------------------------------------------------------- /kubernetes/policies/general/mounts_docker_socket.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV006 2 | 3 | import data.lib.kubernetes 4 | 5 | name = input.metadata.name 6 | 7 | default checkDockerSocket = false 8 | 9 | __rego_metadata__ := { 10 | "id": "KSV006", 11 | "avd_id": "AVD-KSV-0006", 12 | "title": "hostPath volume mounted with docker.sock", 13 | "short_code": "no-docker-sock-mount", 14 | "version": "v1.0.0", 15 | "severity": "HIGH", 16 | "type": "Kubernetes Security Check", 17 | "description": "Mounting docker.sock from the host can give the container full root access to the host.", 18 | "recommended_actions": "Do not specify /var/run/docker.socket in 'spec.template.volumes.hostPath.path'.", 19 | "url": "https://kubesec.io/basics/spec-volumes-hostpath-path-var-run-docker-sock/", 20 | } 21 | 22 | __rego_input__ := { 23 | "combine": false, 24 | "selector": [{"type": "kubernetes"}], 25 | } 26 | 27 | # checkDockerSocket is true if volumes.hostPath.path is set to /var/run/docker.sock 28 | # and is false if volumes.hostPath is set to some other path or not set. 29 | checkDockerSocket { 30 | volumes := kubernetes.volumes 31 | volumes[_].hostPath.path == "/var/run/docker.sock" 32 | } 33 | 34 | deny[res] { 35 | checkDockerSocket 36 | 37 | msg := kubernetes.format(sprintf("%s '%s' should not specify '/var/run/docker.socker' in 'spec.template.volumes.hostPath.path'", [kubernetes.kind, kubernetes.name])) 38 | 39 | res := { 40 | "msg": msg, 41 | "id": __rego_metadata__.id, 42 | "title": __rego_metadata__.title, 43 | "severity": __rego_metadata__.severity, 44 | "type": __rego_metadata__.type, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /kubernetes/policies/general/mounts_docker_socket_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV006 2 | 3 | test_docker_socket_not_mounted_allowed { 4 | r := deny with input as { 5 | "apiVersion": "v1", 6 | "kind": "Pod", 7 | "metadata": {"name": "hello-docker-socket"}, 8 | "spec": {"containers": [{ 9 | "command": [ 10 | "sh", 11 | "-c", 12 | "echo 'Hello' && sleep 1h", 13 | ], 14 | "image": "busybox", 15 | "name": "hello", 16 | }]}, 17 | } 18 | 19 | count(r) == 0 20 | } 21 | 22 | test_docker_socket_mounted_denied { 23 | r := deny with input as { 24 | "apiVersion": "v1", 25 | "kind": "Pod", 26 | "metadata": {"name": "hello-docker-socket"}, 27 | "spec": { 28 | "containers": [{ 29 | "command": [ 30 | "sh", 31 | "-c", 32 | "echo 'Hello' && sleep 1h", 33 | ], 34 | "image": "busybox", 35 | "name": "hello", 36 | }], 37 | "volumes": [{ 38 | "name": "test-volume", 39 | "hostPath": { 40 | "path": "/var/run/docker.sock", 41 | "type": "Directory", 42 | }, 43 | }], 44 | }, 45 | } 46 | 47 | count(r) == 1 48 | r[_].msg == "Pod 'hello-docker-socket' should not specify '/var/run/docker.socker' in 'spec.template.volumes.hostPath.path'" 49 | } 50 | -------------------------------------------------------------------------------- /kubernetes/policies/general/runs_with_GID_le_10000.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV021 2 | 3 | import data.lib.kubernetes 4 | import data.lib.utils 5 | 6 | default failRunAsGroup = false 7 | 8 | __rego_metadata__ := { 9 | "id": "KSV021", 10 | "avd_id": "AVD-KSV-0021", 11 | "title": "Runs with low group ID", 12 | "short_code": "use-high-gid", 13 | "version": "v1.0.0", 14 | "severity": "MEDIUM", 15 | "type": "Kubernetes Security Check", 16 | "description": "Force the container to run with group ID > 10000 to avoid conflicts with the host’s user table.", 17 | "recommended_actions": "Set 'containers[].securityContext.runAsGroup' to an integer > 10000.", 18 | "url": "https://kubesec.io/basics/containers-securitycontext-runasuser/", 19 | } 20 | 21 | __rego_input__ := { 22 | "combine": false, 23 | "selector": [{"type": "kubernetes"}], 24 | } 25 | 26 | # getGroupIdContainers returns the names of all containers which have 27 | # securityContext.runAsGroup less than or equal to 10000. 28 | getGroupIdContainers[container] { 29 | allContainers := kubernetes.containers[_] 30 | allContainers.securityContext.runAsGroup <= 10000 31 | container := allContainers.name 32 | } 33 | 34 | # getGroupIdContainers returns the names of all containers which do 35 | # not have securityContext.runAsGroup set. 36 | getGroupIdContainers[container] { 37 | allContainers := kubernetes.containers[_] 38 | not utils.has_key(allContainers.securityContext, "runAsGroup") 39 | container := allContainers.name 40 | } 41 | 42 | # getGroupIdContainers returns the names of all containers which do 43 | # not have securityContext set. 44 | getGroupIdContainers[container] { 45 | allContainers := kubernetes.containers[_] 46 | not utils.has_key(allContainers, "securityContext") 47 | container := allContainers.name 48 | } 49 | 50 | # failRunAsGroup is true if securityContext.runAsGroup is less than or 51 | # equal to 10000 or if securityContext.runAsGroup is not set. 52 | failRunAsGroup { 53 | count(getGroupIdContainers) > 0 54 | } 55 | 56 | deny[res] { 57 | failRunAsGroup 58 | 59 | msg := kubernetes.format(sprintf("Container '%s' of %s '%s' should set 'securityContext.runAsGroup' > 10000", [getGroupIdContainers[_], kubernetes.kind, kubernetes.name])) 60 | res := { 61 | "msg": msg, 62 | "id": __rego_metadata__.id, 63 | "title": __rego_metadata__.title, 64 | "severity": __rego_metadata__.severity, 65 | "type": __rego_metadata__.type, 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /kubernetes/policies/general/runs_with_GID_le_10000_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV021 2 | 3 | test_GID_gt_10000_allowed { 4 | r := deny with input as { 5 | "apiVersion": "v1", 6 | "kind": "Pod", 7 | "metadata": {"name": "hello-gid"}, 8 | "spec": {"containers": [{ 9 | "command": [ 10 | "sh", 11 | "-c", 12 | "echo 'Hello' && sleep 1h", 13 | ], 14 | "image": "busybox", 15 | "name": "hello", 16 | "securityContext": {"runAsGroup": 10004}, 17 | }]}, 18 | } 19 | 20 | count(r) == 0 21 | } 22 | 23 | test_no_run_as_group_denied { 24 | r := deny with input as { 25 | "apiVersion": "v1", 26 | "kind": "Pod", 27 | "metadata": {"name": "hello-gid"}, 28 | "spec": {"containers": [{ 29 | "command": [ 30 | "sh", 31 | "-c", 32 | "echo 'Hello' && sleep 1h", 33 | ], 34 | "image": "busybox", 35 | "name": "hello", 36 | }]}, 37 | } 38 | 39 | count(r) == 1 40 | r[_].msg == "Container 'hello' of Pod 'hello-gid' should set 'securityContext.runAsGroup' > 10000" 41 | } 42 | 43 | test_low_gid_denied { 44 | r := deny with input as { 45 | "apiVersion": "v1", 46 | "kind": "Pod", 47 | "metadata": {"name": "hello-gid"}, 48 | "spec": {"containers": [{ 49 | "command": [ 50 | "sh", 51 | "-c", 52 | "echo 'Hello' && sleep 1h", 53 | ], 54 | "image": "busybox", 55 | "name": "hello", 56 | "securityContext": {"runAsGroup": 100}, 57 | }]}, 58 | } 59 | 60 | count(r) == 1 61 | r[_].msg == "Container 'hello' of Pod 'hello-gid' should set 'securityContext.runAsGroup' > 10000" 62 | } 63 | -------------------------------------------------------------------------------- /kubernetes/policies/general/runs_with_UID_le_10000.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV020 2 | 3 | import data.lib.kubernetes 4 | import data.lib.utils 5 | 6 | default failRunAsUser = false 7 | 8 | __rego_metadata__ := { 9 | "id": "KSV020", 10 | "avd_id": "AVD-KSV-0020", 11 | "title": "Runs with low user ID", 12 | "short_code": "use-high-uid", 13 | "version": "v1.0.0", 14 | "severity": "MEDIUM", 15 | "type": "Kubernetes Security Check", 16 | "description": "Force the container to run with user ID > 10000 to avoid conflicts with the host’s user table.", 17 | "recommended_actions": "Set 'containers[].securityContext.runAsUser' to an integer > 10000.", 18 | "url": "https://kubesec.io/basics/containers-securitycontext-runasuser/", 19 | } 20 | 21 | __rego_input__ := { 22 | "combine": false, 23 | "selector": [{"type": "kubernetes"}], 24 | } 25 | 26 | # getUserIdContainers returns the names of all containers which have 27 | # securityContext.runAsUser less than or equal to 100000. 28 | getUserIdContainers[container] { 29 | allContainers := kubernetes.containers[_] 30 | allContainers.securityContext.runAsUser <= 10000 31 | container := allContainers.name 32 | } 33 | 34 | # getUserIdContainers returns the names of all containers which do 35 | # not have securityContext.runAsUser set. 36 | getUserIdContainers[container] { 37 | allContainers := kubernetes.containers[_] 38 | not utils.has_key(allContainers.securityContext, "runAsUser") 39 | container := allContainers.name 40 | } 41 | 42 | # getUserIdContainers returns the names of all containers which do 43 | # not have securityContext set. 44 | getUserIdContainers[container] { 45 | allContainers := kubernetes.containers[_] 46 | not utils.has_key(allContainers, "securityContext") 47 | container := allContainers.name 48 | } 49 | 50 | # failRunAsUser is true if securityContext.runAsUser is less than or 51 | # equal to 10000 or if securityContext.runAsUser is not set. 52 | failRunAsUser { 53 | count(getUserIdContainers) > 0 54 | } 55 | 56 | deny[res] { 57 | failRunAsUser 58 | 59 | msg := kubernetes.format(sprintf("Container '%s' of %s '%s' should set 'securityContext.runAsUser' > 10000", [getUserIdContainers[_], kubernetes.kind, kubernetes.name])) 60 | 61 | res := { 62 | "msg": msg, 63 | "id": __rego_metadata__.id, 64 | "title": __rego_metadata__.title, 65 | "severity": __rego_metadata__.severity, 66 | "type": __rego_metadata__.type, 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /kubernetes/policies/general/runs_with_UID_le_10000_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV020 2 | 3 | test_UID_gt_10000_allowed { 4 | r := deny with input as { 5 | "apiVersion": "v1", 6 | "kind": "Pod", 7 | "metadata": {"name": "hello-uid"}, 8 | "spec": {"containers": [{ 9 | "command": [ 10 | "sh", 11 | "-c", 12 | "echo 'Hello' && sleep 1h", 13 | ], 14 | "image": "busybox", 15 | "name": "hello", 16 | "securityContext": {"runAsUser": 10004}, 17 | }]}, 18 | } 19 | 20 | count(r) == 0 21 | } 22 | 23 | test_no_run_as_user_denied { 24 | r := deny with input as { 25 | "apiVersion": "v1", 26 | "kind": "Pod", 27 | "metadata": {"name": "hello-uid"}, 28 | "spec": {"containers": [{ 29 | "command": [ 30 | "sh", 31 | "-c", 32 | "echo 'Hello' && sleep 1h", 33 | ], 34 | "image": "busybox", 35 | "name": "hello", 36 | }]}, 37 | } 38 | 39 | count(r) == 1 40 | r[_].msg == "Container 'hello' of Pod 'hello-uid' should set 'securityContext.runAsUser' > 10000" 41 | } 42 | 43 | test_low_uid_denied { 44 | r := deny with input as { 45 | "apiVersion": "v1", 46 | "kind": "Pod", 47 | "metadata": {"name": "hello-uid"}, 48 | "spec": {"containers": [{ 49 | "command": [ 50 | "sh", 51 | "-c", 52 | "echo 'Hello' && sleep 1h", 53 | ], 54 | "image": "busybox", 55 | "name": "hello", 56 | "securityContext": {"runAsUser": 100}, 57 | }]}, 58 | } 59 | 60 | count(r) == 1 61 | r[_].msg == "Container 'hello' of Pod 'hello-uid' should set 'securityContext.runAsUser' > 10000" 62 | } 63 | 64 | test_zero_uid_denied { 65 | r := deny with input as { 66 | "apiVersion": "v1", 67 | "kind": "Pod", 68 | "metadata": {"name": "hello-uid"}, 69 | "spec": {"containers": [{ 70 | "command": [ 71 | "sh", 72 | "-c", 73 | "echo 'Hello' && sleep 1h", 74 | ], 75 | "image": "busybox", 76 | "name": "hello", 77 | "securityContext": {"runAsUser": 0}, 78 | }]}, 79 | } 80 | 81 | count(r) == 1 82 | r[_].msg == "Container 'hello' of Pod 'hello-uid' should set 'securityContext.runAsUser' > 10000" 83 | } 84 | -------------------------------------------------------------------------------- /kubernetes/policies/general/tiller_is_deployed.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV202 2 | 3 | import data.lib.kubernetes 4 | 5 | __rego_metadata__ := { 6 | "id": "KSV102", 7 | "avd_id": "AVD-KSV-0102", 8 | "title": "Tiller Is Deployed", 9 | "short_code": "no-tiller", 10 | "version": "v1.0.0", 11 | "severity": "Critical", 12 | "type": "Kubernetes Security Check", 13 | "description": "Check if Helm Tiller component is deployed.", 14 | "recommended_actions": "Migrate to Helm v3 which no longer has Tiller component", 15 | } 16 | 17 | __rego_input__ := { 18 | "combine": false, 19 | "selector": [{"type": "kubernetes"}], 20 | } 21 | 22 | # Get all containers and check kubernetes metadata for tiller 23 | tillerDeployed[container] { 24 | currentContainer := kubernetes.containers[_] 25 | checkMetadata(input.metadata) 26 | container := currentContainer.name 27 | } 28 | 29 | # Get all containers and check each image for tiller 30 | tillerDeployed[container] { 31 | currentContainer := kubernetes.containers[_] 32 | contains(currentContainer.image, "tiller") 33 | container := currentContainer.name 34 | } 35 | 36 | # Get all pods and check each metadata for tiller 37 | tillerDeployed[pod] { 38 | currentPod := kubernetes.pods[_] 39 | checkMetadata(currentPod.metadata) 40 | pod := currentPod.metadata.name 41 | } 42 | 43 | deny[res] { 44 | msg := kubernetes.format(sprintf("container '%s' of %s '%s' in '%s' namespace shouldn't have tiller deployed", [tillerDeployed[_], lower(kubernetes.kind), kubernetes.name, kubernetes.namespace])) 45 | 46 | res := { 47 | "msg": msg, 48 | "id": __rego_metadata__.id, 49 | "title": __rego_metadata__.title, 50 | "severity": __rego_metadata__.severity, 51 | "type": __rego_metadata__.type, 52 | } 53 | } 54 | 55 | # Check for tiller by resource name 56 | checkMetadata(metadata) { 57 | contains(metadata.name, "tiller") 58 | } 59 | 60 | # Check for tiller by app label 61 | checkMetadata(metadata) { 62 | metadata.labels.app == "helm" 63 | } 64 | 65 | # Check for tiller by name label 66 | checkMetadata(metadata) { 67 | metadata.labels.name == "tiller" 68 | } 69 | -------------------------------------------------------------------------------- /kubernetes/policies/general/uses_image_tag_latest.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV013 2 | 3 | import data.lib.kubernetes 4 | 5 | default checkUsingLatestTag = false 6 | 7 | __rego_metadata__ := { 8 | "id": "KSV013", 9 | "avd_id": "AVD-KSV-0013", 10 | "title": "Image tag ':latest' used", 11 | "short_code": "use-specific-tags", 12 | "version": "v1.0.0", 13 | "severity": "LOW", 14 | "type": "Kubernetes Security Check", 15 | "description": "It is best to avoid using the ':latest' image tag when deploying containers in production. Doing so makes it hard to track which version of the image is running, and hard to roll back the version.", 16 | "recommended_actions": "Use a specific container image tag that is not 'latest'.", 17 | "url": "https://kubernetes.io/docs/concepts/configuration/overview/#container-images", 18 | } 19 | 20 | __rego_input__ := { 21 | "combine": false, 22 | "selector": [{"type": "kubernetes"}], 23 | } 24 | 25 | # getTaggedContainers returns the names of all containers which 26 | # have tagged images. 27 | getTaggedContainers[container] { 28 | # If the image defines a digest value, we don't care about the tag 29 | allContainers := kubernetes.containers[_] 30 | digest := split(allContainers.image, "@")[1] 31 | container := allContainers.name 32 | } 33 | 34 | getTaggedContainers[container] { 35 | # No digest, look at tag 36 | allContainers := kubernetes.containers[_] 37 | tag := split(allContainers.image, ":")[1] 38 | tag != "latest" 39 | container := allContainers.name 40 | } 41 | 42 | # getUntaggedContainers returns the names of all containers which 43 | # have untagged images or images with the latest tag. 44 | getUntaggedContainers[container] { 45 | container := kubernetes.containers[_].name 46 | not getTaggedContainers[container] 47 | } 48 | 49 | # checkUsingLatestTag is true if there is a container whose image tag 50 | # is untagged or uses the latest tag. 51 | checkUsingLatestTag { 52 | count(getUntaggedContainers) > 0 53 | } 54 | 55 | deny[res] { 56 | checkUsingLatestTag 57 | 58 | msg := kubernetes.format(sprintf("Container '%s' of %s '%s' should specify an image tag", [getUntaggedContainers[_], kubernetes.kind, kubernetes.name])) 59 | 60 | res := { 61 | "msg": msg, 62 | "id": __rego_metadata__.id, 63 | "title": __rego_metadata__.title, 64 | "severity": __rego_metadata__.severity, 65 | "type": __rego_metadata__.type, 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /kubernetes/policies/pss/baseline/1_host_ipc.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV008 2 | 3 | import data.lib.kubernetes 4 | 5 | default failHostIPC = false 6 | 7 | __rego_metadata__ := { 8 | "id": "KSV008", 9 | "avd_id": "AVD-KSV-0008", 10 | "title": "Access to host IPC namespace", 11 | "short_code": "no-shared-ipc-namespace", 12 | "version": "v1.0.0", 13 | "severity": "HIGH", 14 | "type": "Kubernetes Security Check", 15 | "description": "Sharing the host’s IPC namespace allows container processes to communicate with processes on the host.", 16 | "recommended_actions": "Do not set 'spec.template.spec.hostIPC' to true.", 17 | "url": "https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline", 18 | } 19 | 20 | __rego_input__ := { 21 | "combine": false, 22 | "selector": [{"type": "kubernetes"}], 23 | } 24 | 25 | # failHostIPC is true if spec.hostIPC is set to true (on all resources) 26 | failHostIPC { 27 | kubernetes.host_ipcs[_] == true 28 | } 29 | 30 | deny[res] { 31 | failHostIPC 32 | 33 | msg := kubernetes.format(sprintf("%s '%s' should not set 'spec.template.spec.hostIPC' to true", [kubernetes.kind, kubernetes.name])) 34 | 35 | res := { 36 | "msg": msg, 37 | "id": __rego_metadata__.id, 38 | "title": __rego_metadata__.title, 39 | "severity": __rego_metadata__.severity, 40 | "type": __rego_metadata__.type, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /kubernetes/policies/pss/baseline/1_host_ipc_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV008 2 | 3 | test_host_ipc_set_to_true_denied { 4 | r := deny with input as { 5 | "apiVersion": "v1", 6 | "kind": "Pod", 7 | "metadata": {"name": "hello-ipc"}, 8 | "spec": { 9 | "hostIPC": true, 10 | "containers": [{ 11 | "command": [ 12 | "sh", 13 | "-c", 14 | "echo 'Hello' && sleep 1h", 15 | ], 16 | "image": "busybox", 17 | "name": "hello", 18 | }], 19 | }, 20 | } 21 | 22 | count(r) == 1 23 | r[_].msg == "Pod 'hello-ipc' should not set 'spec.template.spec.hostIPC' to true" 24 | } 25 | 26 | test_host_ipc_set_to_false_allowed { 27 | r := deny with input as { 28 | "apiVersion": "v1", 29 | "kind": "Pod", 30 | "metadata": {"name": "hello-ipc"}, 31 | "spec": { 32 | "hostIPC": false, 33 | "containers": [{ 34 | "command": [ 35 | "sh", 36 | "-c", 37 | "echo 'Hello' && sleep 1h", 38 | ], 39 | "image": "busybox", 40 | "name": "hello", 41 | }], 42 | }, 43 | } 44 | 45 | count(r) == 0 46 | } 47 | 48 | test_host_ipc_is_undefined_allowed { 49 | r := deny with input as { 50 | "apiVersion": "v1", 51 | "kind": "Pod", 52 | "metadata": {"name": "hello-ipc"}, 53 | "spec": {"containers": [{ 54 | "command": [ 55 | "sh", 56 | "-c", 57 | "echo 'Hello' && sleep 1h", 58 | ], 59 | "image": "busybox", 60 | "name": "hello", 61 | }]}, 62 | } 63 | 64 | count(r) == 0 65 | } 66 | -------------------------------------------------------------------------------- /kubernetes/policies/pss/baseline/1_host_network.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV009 2 | 3 | import data.lib.kubernetes 4 | 5 | default failHostNetwork = false 6 | 7 | __rego_metadata__ := { 8 | "id": "KSV009", 9 | "avd_id": "AVD-KSV-0009", 10 | "title": "Access to host network", 11 | "short_code": "no-host-network", 12 | "version": "v1.0.0", 13 | "severity": "HIGH", 14 | "type": "Kubernetes Security Check", 15 | "description": "Sharing the host’s network namespace permits processes in the pod to communicate with processes bound to the host’s loopback adapter.", 16 | "recommended_actions": "Do not set 'spec.template.spec.hostNetwork' to true.", 17 | "url": "https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline", 18 | } 19 | 20 | __rego_input__ := { 21 | "combine": false, 22 | "selector": [{"type": "kubernetes"}], 23 | } 24 | 25 | # failHostNetwork is true if spec.hostNetwork is set to true (on all controllers) 26 | failHostNetwork { 27 | kubernetes.host_networks[_] == true 28 | } 29 | 30 | deny[res] { 31 | failHostNetwork 32 | 33 | msg := kubernetes.format(sprintf("%s '%s' should not set 'spec.template.spec.hostNetwork' to true", [kubernetes.kind, kubernetes.name])) 34 | 35 | res := { 36 | "msg": msg, 37 | "id": __rego_metadata__.id, 38 | "title": __rego_metadata__.title, 39 | "severity": __rego_metadata__.severity, 40 | "type": __rego_metadata__.type, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /kubernetes/policies/pss/baseline/1_host_network_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV009 2 | 3 | test_host_network_set_to_true_denied { 4 | r := deny with input as { 5 | "apiVersion": "v1", 6 | "kind": "Pod", 7 | "metadata": {"name": "hello-host-network"}, 8 | "spec": { 9 | "hostNetwork": true, 10 | "containers": [{ 11 | "command": [ 12 | "sh", 13 | "-c", 14 | "echo 'Hello' && sleep 1h", 15 | ], 16 | "image": "busybox", 17 | "name": "hello", 18 | }], 19 | }, 20 | } 21 | 22 | count(r) == 1 23 | r[_].msg == "Pod 'hello-host-network' should not set 'spec.template.spec.hostNetwork' to true" 24 | } 25 | 26 | test_host_network_set_to_false_allowed { 27 | r := deny with input as { 28 | "apiVersion": "v1", 29 | "kind": "Pod", 30 | "metadata": {"name": "hello-host-network"}, 31 | "spec": { 32 | "hostNetwork": false, 33 | "containers": [{ 34 | "command": [ 35 | "sh", 36 | "-c", 37 | "echo 'Hello' && sleep 1h", 38 | ], 39 | "image": "busybox", 40 | "name": "hello", 41 | }], 42 | }, 43 | } 44 | 45 | count(r) == 0 46 | } 47 | 48 | test_host_network_is_undefined_allowed { 49 | r := deny with input as { 50 | "apiVersion": "v1", 51 | "kind": "Pod", 52 | "metadata": {"name": "hello-host-network"}, 53 | "spec": {"containers": [{ 54 | "command": [ 55 | "sh", 56 | "-c", 57 | "echo 'Hello' && sleep 1h", 58 | ], 59 | "image": "busybox", 60 | "name": "hello", 61 | }]}, 62 | } 63 | 64 | count(r) == 0 65 | } 66 | -------------------------------------------------------------------------------- /kubernetes/policies/pss/baseline/1_host_pid.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV010 2 | 3 | import data.lib.kubernetes 4 | 5 | default failHostPID = false 6 | 7 | __rego_metadata__ := { 8 | "id": "KSV010", 9 | "avd_id": "AVD-KSV-0010", 10 | "title": "Access to host PID", 11 | "short_code": "no-host-pid", 12 | "version": "v1.0.0", 13 | "severity": "HIGH", 14 | "type": "Kubernetes Security Check", 15 | "description": "Sharing the host’s PID namespace allows visibility on host processes, potentially leaking information such as environment variables and configuration.", 16 | "recommended_actions": "Do not set 'spec.template.spec.hostPID' to true.", 17 | "url": "https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline", 18 | } 19 | 20 | __rego_input__ := { 21 | "combine": false, 22 | "selector": [{"type": "kubernetes"}], 23 | } 24 | 25 | # failHostPID is true if spec.hostPID is set to true (on all controllers) 26 | failHostPID { 27 | kubernetes.host_pids[_] == true 28 | } 29 | 30 | deny[res] { 31 | failHostPID 32 | 33 | msg := kubernetes.format(sprintf("%s '%s' should not set 'spec.template.spec.hostPID' to true", [kubernetes.kind, kubernetes.name])) 34 | 35 | res := { 36 | "msg": msg, 37 | "id": __rego_metadata__.id, 38 | "title": __rego_metadata__.title, 39 | "severity": __rego_metadata__.severity, 40 | "type": __rego_metadata__.type, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /kubernetes/policies/pss/baseline/1_host_pid_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV010 2 | 3 | test_host_pid_set_to_true_denied { 4 | r := deny with input as { 5 | "apiVersion": "v1", 6 | "kind": "Pod", 7 | "metadata": {"name": "hello-host-pid"}, 8 | "spec": { 9 | "hostPID": true, 10 | "containers": [{ 11 | "command": [ 12 | "sh", 13 | "-c", 14 | "echo 'Hello' && sleep 1h", 15 | ], 16 | "image": "busybox", 17 | "name": "hello", 18 | }], 19 | }, 20 | } 21 | 22 | count(r) == 1 23 | r[_].msg == "Pod 'hello-host-pid' should not set 'spec.template.spec.hostPID' to true" 24 | } 25 | 26 | test_host_pid_set_to_false_allowed { 27 | r := deny with input as { 28 | "apiVersion": "v1", 29 | "kind": "Pod", 30 | "metadata": {"name": "hello-host-pid"}, 31 | "spec": { 32 | "hostPID": false, 33 | "containers": [{ 34 | "command": [ 35 | "sh", 36 | "-c", 37 | "echo 'Hello' && sleep 1h", 38 | ], 39 | "image": "busybox", 40 | "name": "hello", 41 | }], 42 | }, 43 | } 44 | 45 | count(r) == 0 46 | } 47 | 48 | test_host_pid_is_undefined_allowed { 49 | r := deny with input as { 50 | "apiVersion": "v1", 51 | "kind": "Pod", 52 | "metadata": {"name": "hello-host-pid"}, 53 | "spec": {"containers": [{ 54 | "command": [ 55 | "sh", 56 | "-c", 57 | "echo 'Hello' && sleep 1h", 58 | ], 59 | "image": "busybox", 60 | "name": "hello", 61 | }]}, 62 | } 63 | 64 | count(r) == 0 65 | } 66 | -------------------------------------------------------------------------------- /kubernetes/policies/pss/baseline/2_privileged.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV017 2 | 3 | import data.lib.kubernetes 4 | 5 | default failPrivileged = false 6 | 7 | __rego_metadata__ := { 8 | "id": "KSV017", 9 | "avd_id": "AVD-KSV-0017", 10 | "title": "Privileged container", 11 | "short_code": "no-privileged-containers", 12 | "version": "v1.0.0", 13 | "severity": "HIGH", 14 | "type": "Kubernetes Security Check", 15 | "description": "Privileged containers share namespaces with the host system and do not offer any security. They should be used exclusively for system containers that require high privileges.", 16 | "recommended_actions": "Change 'containers[].securityContext.privileged' to 'false'.", 17 | "url": "https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline", 18 | } 19 | 20 | __rego_input__ := { 21 | "combine": false, 22 | "selector": [{"type": "kubernetes"}], 23 | } 24 | 25 | # getPrivilegedContainers returns all containers which have 26 | # securityContext.privileged set to true. 27 | getPrivilegedContainers[container] { 28 | allContainers := kubernetes.containers[_] 29 | allContainers.securityContext.privileged == true 30 | container := allContainers.name 31 | } 32 | 33 | # failPrivileged is true if there is ANY container with securityContext.privileged 34 | # set to true. 35 | failPrivileged { 36 | count(getPrivilegedContainers) > 0 37 | } 38 | 39 | deny[res] { 40 | failPrivileged 41 | 42 | msg := kubernetes.format(sprintf("Container '%s' of %s '%s' should set 'securityContext.privileged' to false", [getPrivilegedContainers[_], kubernetes.kind, kubernetes.name])) 43 | res := { 44 | "msg": msg, 45 | "id": __rego_metadata__.id, 46 | "title": __rego_metadata__.title, 47 | "severity": __rego_metadata__.severity, 48 | "type": __rego_metadata__.type, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /kubernetes/policies/pss/baseline/2_privileged_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV017 2 | 3 | test_privileged_is_true_denied { 4 | r := deny with input as { 5 | "apiVersion": "v1", 6 | "kind": "Pod", 7 | "metadata": {"name": "hello-privileged"}, 8 | "spec": {"containers": [{ 9 | "command": [ 10 | "sh", 11 | "-c", 12 | "echo 'Hello' && sleep 1h", 13 | ], 14 | "image": "busybox", 15 | "name": "hello", 16 | "securityContext": {"privileged": true}, 17 | }]}, 18 | } 19 | 20 | count(r) == 1 21 | r[_].msg == "Container 'hello' of Pod 'hello-privileged' should set 'securityContext.privileged' to false" 22 | } 23 | 24 | test_privileged_is_undefined_allowed { 25 | r := deny with input as { 26 | "apiVersion": "v1", 27 | "kind": "Pod", 28 | "metadata": {"name": "hello-privileged"}, 29 | "spec": {"containers": [{ 30 | "command": [ 31 | "sh", 32 | "-c", 33 | "echo 'Hello' && sleep 1h", 34 | ], 35 | "image": "busybox", 36 | "name": "hello", 37 | }]}, 38 | } 39 | 40 | count(r) == 0 41 | } 42 | 43 | test_privileged_is_false_allowed { 44 | r := deny with input as { 45 | "apiVersion": "v1", 46 | "kind": "Pod", 47 | "metadata": {"name": "hello-privileged"}, 48 | "spec": {"containers": [{ 49 | "command": [ 50 | "sh", 51 | "-c", 52 | "echo 'Hello' && sleep 1h", 53 | ], 54 | "image": "busybox", 55 | "name": "hello", 56 | "securityContext": {"privileged": false}, 57 | }]}, 58 | } 59 | 60 | count(r) == 0 61 | } 62 | -------------------------------------------------------------------------------- /kubernetes/policies/pss/baseline/3_specific_capabilities_added.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV022 2 | 3 | import data.lib.kubernetes 4 | 5 | default failAdditionalCaps = false 6 | 7 | __rego_metadata__ := { 8 | "id": "KSV022", 9 | "avd_id": "AVD-KSV-0022", 10 | "title": "Non-default capabilities added", 11 | "short_code": "no-non-default-capabilities", 12 | "version": "v1.0.0", 13 | "severity": "MEDIUM", 14 | "type": "Kubernetes Security Check", 15 | "description": "Adding NET_RAW or capabilities beyond the default set must be disallowed.", 16 | "recommended_actions": "Do not set spec.containers[*].securityContext.capabilities.add and spec.initContainers[*].securityContext.capabilities.add", 17 | "url": "https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline", 18 | } 19 | 20 | __rego_input__ := { 21 | "combine": false, 22 | "selector": [{"type": "kubernetes"}], 23 | } 24 | 25 | # Add allowed capabilities to this set 26 | allowed_caps = set() 27 | 28 | # getContainersWithDisallowedCaps returns a list of containers which have 29 | # additional capabilities not included in the allowed capabilities list 30 | getContainersWithDisallowedCaps[container] { 31 | allContainers := kubernetes.containers[_] 32 | set_caps := {cap | cap := allContainers.securityContext.capabilities.add[_]} 33 | caps_not_allowed := set_caps - allowed_caps 34 | count(caps_not_allowed) > 0 35 | container := allContainers.name 36 | } 37 | 38 | # cap_msg is a string of allowed capabilities to be print as part of deny message 39 | caps_msg = "" { 40 | count(allowed_caps) == 0 41 | } else = msg { 42 | msg := sprintf(" or set it to the following allowed values: %s", [concat(", ", allowed_caps)]) 43 | } 44 | 45 | # failAdditionalCaps is true if there are containers which set additional capabilities 46 | # not included in the allowed capabilities list 47 | failAdditionalCaps { 48 | count(getContainersWithDisallowedCaps) > 0 49 | } 50 | 51 | deny[res] { 52 | failAdditionalCaps 53 | 54 | msg := sprintf("Container '%s' of %s '%s' should not set 'securityContext.capabilities.add'%s", [getContainersWithDisallowedCaps[_], kubernetes.kind, kubernetes.name, caps_msg]) 55 | res := { 56 | "msg": msg, 57 | "id": __rego_metadata__.id, 58 | "title": __rego_metadata__.title, 59 | "severity": __rego_metadata__.severity, 60 | "type": __rego_metadata__.type, 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /kubernetes/policies/pss/baseline/3_specific_capabilities_added_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV022 2 | 3 | test_capabilities_add_denied { 4 | r := deny with input as { 5 | "apiVersion": "v1", 6 | "kind": "Pod", 7 | "metadata": {"name": "hello-add-capabilities"}, 8 | "spec": {"containers": [{ 9 | "command": [ 10 | "sh", 11 | "-c", 12 | "echo 'Hello' && sleep 1h", 13 | ], 14 | "image": "busybox", 15 | "name": "hello", 16 | "securityContext": {"capabilities": {"add": ["NET_BIND_SERVICE"]}}, 17 | }]}, 18 | } 19 | 20 | count(r) == 1 21 | r[_].msg == "Container 'hello' of Pod 'hello-add-capabilities' should not set 'securityContext.capabilities.add'" 22 | } 23 | 24 | test_capabilities_add_empty_allowed { 25 | r := deny with input as { 26 | "apiVersion": "v1", 27 | "kind": "Pod", 28 | "metadata": {"name": "hello-add-capabilities"}, 29 | "spec": {"containers": [{ 30 | "command": [ 31 | "sh", 32 | "-c", 33 | "echo 'Hello' && sleep 1h", 34 | ], 35 | "image": "busybox", 36 | "name": "hello", 37 | "securityContext": {"capabilities": {"add": []}}, 38 | }]}, 39 | } 40 | 41 | count(r) == 0 42 | } 43 | 44 | test_capabilities_no_add_allowed { 45 | r := deny with input as { 46 | "apiVersion": "v1", 47 | "kind": "Pod", 48 | "metadata": {"name": "hello-add-capabilities"}, 49 | "spec": {"containers": [{ 50 | "command": [ 51 | "sh", 52 | "-c", 53 | "echo 'Hello' && sleep 1h", 54 | ], 55 | "image": "busybox", 56 | "name": "hello", 57 | "securityContext": {"capabilities": {}}, 58 | }]}, 59 | } 60 | 61 | count(r) == 0 62 | } 63 | 64 | test_no_capabilities_allowed { 65 | r := deny with input as { 66 | "apiVersion": "v1", 67 | "kind": "Pod", 68 | "metadata": {"name": "hello-add-capabilities"}, 69 | "spec": {"containers": [{ 70 | "command": [ 71 | "sh", 72 | "-c", 73 | "echo 'Hello' && sleep 1h", 74 | ], 75 | "image": "busybox", 76 | "name": "hello", 77 | "securityContext": {}, 78 | }]}, 79 | } 80 | 81 | count(r) == 0 82 | } 83 | -------------------------------------------------------------------------------- /kubernetes/policies/pss/baseline/4_hostpath_volumes_mounted.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV023 2 | 3 | import data.lib.kubernetes 4 | import data.lib.utils 5 | 6 | default failHostPathVolume = false 7 | 8 | __rego_metadata__ := { 9 | "id": "KSV023", 10 | "avd_id": "AVD-KSV-0023", 11 | "title": "hostPath volumes mounted", 12 | "short_code": "no-mounted-hostpath", 13 | "version": "v1.0.0", 14 | "severity": "MEDIUM", 15 | "type": "Kubernetes Security Check", 16 | "description": "HostPath volumes must be forbidden.", 17 | "recommended_actions": "Do not set 'spec.volumes[*].hostPath'.", 18 | "url": "https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline", 19 | } 20 | 21 | __rego_input__ := { 22 | "combine": false, 23 | "selector": [{"type": "kubernetes"}], 24 | } 25 | 26 | failHostPathVolume { 27 | volumes := kubernetes.volumes 28 | utils.has_key(volumes[_], "hostPath") 29 | } 30 | 31 | deny[res] { 32 | failHostPathVolume 33 | 34 | msg := kubernetes.format(sprintf("%s '%s' should not set 'spec.template.volumes.hostPath'", [kubernetes.kind, kubernetes.name])) 35 | 36 | res := { 37 | "msg": msg, 38 | "id": __rego_metadata__.id, 39 | "title": __rego_metadata__.title, 40 | "severity": __rego_metadata__.severity, 41 | "type": __rego_metadata__.type, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /kubernetes/policies/pss/baseline/4_hostpath_volumes_mounted_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV023 2 | 3 | test_host_path_specified_denied { 4 | r := deny with input as { 5 | "apiVersion": "v1", 6 | "kind": "Pod", 7 | "metadata": {"name": "hello-host-path"}, 8 | "spec": { 9 | "containers": [{ 10 | "command": [ 11 | "sh", 12 | "-c", 13 | "echo 'Hello' && sleep 1h", 14 | ], 15 | "image": "busybox", 16 | "name": "hello", 17 | }], 18 | "volumes": [{"hostPath": { 19 | "path": "/sys", 20 | "type": "", 21 | }}], 22 | }, 23 | } 24 | 25 | count(r) == 1 26 | r[_].msg == "Pod 'hello-host-path' should not set 'spec.template.volumes.hostPath'" 27 | } 28 | 29 | test_host_path_not_specified_allowed { 30 | r := deny with input as { 31 | "apiVersion": "v1", 32 | "kind": "Pod", 33 | "metadata": {"name": "hello-host-path"}, 34 | "spec": { 35 | "containers": [{ 36 | "command": [ 37 | "sh", 38 | "-c", 39 | "echo 'Hello' && sleep 1h", 40 | ], 41 | "image": "busybox", 42 | "name": "hello", 43 | }], 44 | "volumes": [{"name": "my-vol"}], 45 | }, 46 | } 47 | 48 | count(r) == 0 49 | } 50 | -------------------------------------------------------------------------------- /kubernetes/policies/pss/baseline/5_access_to_host_ports.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV024 2 | 3 | import data.lib.kubernetes 4 | 5 | default failHostPorts = false 6 | 7 | __rego_metadata__ := { 8 | "id": "KSV024", 9 | "avd_id": "AVD-KSV-0024", 10 | "title": "Access to host ports", 11 | "short_code": "no-host-port-access", 12 | "version": "v1.0.0", 13 | "severity": "HIGH", 14 | "type": "Kubernetes Security Check", 15 | "description": "HostPorts should be disallowed, or at minimum restricted to a known list.", 16 | "recommended_actions": "Do not set spec.containers[*].ports[*].hostPort and spec.initContainers[*].ports[*].hostPort.", 17 | "url": "https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline", 18 | } 19 | 20 | __rego_input__ := { 21 | "combine": false, 22 | "selector": [{"type": "kubernetes"}], 23 | } 24 | 25 | # Add allowed host ports to this set 26 | allowed_host_ports = set() 27 | 28 | # getContainersWithDisallowedHostPorts returns a list of containers which have 29 | # host ports not included in the allowed host port list 30 | getContainersWithDisallowedHostPorts[container] { 31 | allContainers := kubernetes.containers[_] 32 | set_host_ports := {port | port := allContainers.ports[_].hostPort} 33 | host_ports_not_allowed := set_host_ports - allowed_host_ports 34 | count(host_ports_not_allowed) > 0 35 | container := allContainers.name 36 | } 37 | 38 | # host_ports_msg is a string of allowed host ports to be print as part of deny message 39 | host_ports_msg = "" { 40 | count(allowed_host_ports) == 0 41 | } else = msg { 42 | msg := sprintf(" or set it to the following allowed values: %s", [concat(", ", allowed_host_ports)]) 43 | } 44 | 45 | # failHostPorts is true if there are containers which set host ports 46 | # not included in the allowed host ports list 47 | failHostPorts { 48 | count(getContainersWithDisallowedHostPorts) > 0 49 | } 50 | 51 | deny[res] { 52 | failHostPorts 53 | 54 | msg := sprintf("Container '%s' of %s '%s' should not set host ports, 'ports[*].hostPort'%s", [getContainersWithDisallowedHostPorts[_], kubernetes.kind, kubernetes.name, host_ports_msg]) 55 | res := { 56 | "msg": msg, 57 | "id": __rego_metadata__.id, 58 | "title": __rego_metadata__.title, 59 | "severity": __rego_metadata__.severity, 60 | "type": __rego_metadata__.type, 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /kubernetes/policies/pss/baseline/5_access_to_host_ports_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV024 2 | 3 | test_host_ports_defined_denied { 4 | r := deny with input as { 5 | "apiVersion": "v1", 6 | "kind": "Pod", 7 | "metadata": {"name": "hello-host-ports"}, 8 | "spec": {"containers": [{ 9 | "command": [ 10 | "sh", 11 | "-c", 12 | "echo 'Hello' && sleep 1h", 13 | ], 14 | "image": "busybox", 15 | "name": "hello", 16 | "ports": [{"hostPort": 8080}], 17 | }]}, 18 | } 19 | 20 | count(r) == 1 21 | r[_].msg == "Container 'hello' of Pod 'hello-host-ports' should not set host ports, 'ports[*].hostPort'" 22 | } 23 | 24 | test_no_host_ports_defined_allowed { 25 | r := deny with input as { 26 | "apiVersion": "v1", 27 | "kind": "Pod", 28 | "metadata": {"name": "hello-host-ports"}, 29 | "spec": {"containers": [{ 30 | "command": [ 31 | "sh", 32 | "-c", 33 | "echo 'Hello' && sleep 1h", 34 | ], 35 | "image": "busybox", 36 | "name": "hello", 37 | "ports": [{"containerPort": 80}], 38 | }]}, 39 | } 40 | 41 | count(r) == 0 42 | } 43 | -------------------------------------------------------------------------------- /kubernetes/policies/pss/baseline/6_apparmor_policy_disabled.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV002 2 | 3 | import data.lib.kubernetes 4 | 5 | default failAppArmor = false 6 | 7 | __rego_metadata__ := { 8 | "id": "KSV002", 9 | "avd_id": "AVD-KSV-0002", 10 | "title": "Default AppArmor profile not set", 11 | "short_code": "use-default-apparmor-profile", 12 | "version": "v1.0.0", 13 | "severity": "MEDIUM", 14 | "type": "Kubernetes Security Check", 15 | "description": "A program inside the container can bypass AppArmor protection policies.", 16 | "recommended_actions": "Remove 'container.apparmor.security.beta.kubernetes.io' annotation or set it to 'runtime/default'.", 17 | "url": "https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline", 18 | } 19 | 20 | __rego_input__ := { 21 | "combine": false, 22 | "selector": [{"type": "kubernetes"}], 23 | } 24 | 25 | apparmor_keys[container] = key { 26 | container := kubernetes.containers[_].name 27 | key := sprintf("%s/%s", ["container.apparmor.security.beta.kubernetes.io", container]) 28 | } 29 | 30 | custom_apparmor_containers[container] { 31 | key := apparmor_keys[container] 32 | annotations := kubernetes.annotations[_] 33 | val := annotations[key] 34 | val != "runtime/default" 35 | } 36 | 37 | deny[res] { 38 | container := custom_apparmor_containers[_] 39 | 40 | msg := kubernetes.format(sprintf("Container '%s' of %s '%s' should specify an AppArmor profile", [container, kubernetes.kind, kubernetes.name])) 41 | 42 | res := { 43 | "msg": msg, 44 | "id": __rego_metadata__.id, 45 | "title": __rego_metadata__.title, 46 | "severity": __rego_metadata__.severity, 47 | "type": __rego_metadata__.type, 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /kubernetes/policies/pss/baseline/6_apparmor_policy_disabled_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV002 2 | 3 | import data.lib.kubernetes 4 | 5 | test_custom_deny { 6 | r := deny with input as { 7 | "apiVersion": "v1", 8 | "kind": "Pod", 9 | "metadata": { 10 | "annotations": {"container.apparmor.security.beta.kubernetes.io/hello": "custom"}, 11 | "name": "hello-apparmor", 12 | }, 13 | "spec": {"containers": [{ 14 | "command": [ 15 | "sh", 16 | "-c", 17 | "echo 'Hello AppArmor!' && sleep 1h", 18 | ], 19 | "image": "busybox", 20 | "name": "hello", 21 | }]}, 22 | } 23 | 24 | count(r) == 1 25 | r[_].msg == "Container 'hello' of Pod 'hello-apparmor' should specify an AppArmor profile" 26 | } 27 | 28 | test_undefined_allowed { 29 | r := deny with input as { 30 | "apiVersion": "v1", 31 | "kind": "Pod", 32 | "metadata": {"name": "hello-apparmor"}, 33 | "spec": {"containers": [{ 34 | "command": [ 35 | "sh", 36 | "-c", 37 | "echo 'Hello AppArmor!' && sleep 1h", 38 | ], 39 | "image": "busybox", 40 | "name": "hello", 41 | }]}, 42 | } 43 | 44 | count(r) == 0 45 | } 46 | 47 | test_only_one_is_undefined_allowed { 48 | r := deny with input as { 49 | "apiVersion": "v1", 50 | "kind": "Pod", 51 | "metadata": { 52 | "annotations": {"container.apparmor.security.beta.kubernetes.io/hello2": "runtime/default"}, 53 | "name": "hello-apparmor", 54 | }, 55 | "spec": {"containers": [ 56 | { 57 | "command": [ 58 | "sh", 59 | "-c", 60 | "echo 'Hello AppArmor!' && sleep 1h", 61 | ], 62 | "image": "busybox", 63 | "name": "hello", 64 | }, 65 | { 66 | "command": [ 67 | "sh", 68 | "-c", 69 | "echo 'Hello AppArmor Again!' && sleep 1h", 70 | ], 71 | "image": "busybox", 72 | "name": "hello2", 73 | }, 74 | ]}, 75 | } 76 | 77 | count(r) == 0 78 | } 79 | 80 | test_runtime_default_allowed { 81 | r := deny with input as { 82 | "apiVersion": "v1", 83 | "kind": "Pod", 84 | "metadata": { 85 | "annotations": {"container.apparmor.security.beta.kubernetes.io/hello": "runtime/default"}, 86 | "name": "hello-apparmor", 87 | }, 88 | "spec": {"containers": [{ 89 | "command": [ 90 | "sh", 91 | "-c", 92 | "echo 'Hello AppArmor!' && sleep 1h", 93 | ], 94 | "image": "busybox", 95 | "name": "hello", 96 | }]}, 97 | } 98 | 99 | count(r) == 0 100 | } 101 | -------------------------------------------------------------------------------- /kubernetes/policies/pss/baseline/8_non_default_proc_masks_set.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV027 2 | 3 | import data.lib.kubernetes 4 | import data.lib.utils 5 | 6 | default failProcMount = false 7 | 8 | __rego_metadata__ := { 9 | "id": "KSV027", 10 | "avd_id": "AVD-KSV-0027", 11 | "title": "Non-default /proc masks set", 12 | "short_code": "no-custom-proc-mask", 13 | "version": "v1.0.0", 14 | "severity": "MEDIUM", 15 | "type": "Kubernetes Security Check", 16 | "description": "The default /proc masks are set up to reduce attack surface, and should be required.", 17 | "recommended_actions": "Do not set spec.containers[*].securityContext.procMount and spec.initContainers[*].securityContext.procMount.", 18 | "url": "https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline", 19 | } 20 | 21 | __rego_input__ := { 22 | "combine": false, 23 | "selector": [{"type": "kubernetes"}], 24 | } 25 | 26 | # failProcMountOpts is true if securityContext.procMount is set in any container 27 | failProcMountOpts { 28 | allContainers := kubernetes.containers[_] 29 | utils.has_key(allContainers.securityContext, "procMount") 30 | } 31 | 32 | deny[res] { 33 | failProcMountOpts 34 | 35 | msg := kubernetes.format(sprintf("%s '%s' should not set 'spec.containers[*].securityContext.procMount' or 'spec.initContainers[*].securityContext.procMount'", [kubernetes.kind, kubernetes.name])) 36 | 37 | res := { 38 | "msg": msg, 39 | "id": __rego_metadata__.id, 40 | "title": __rego_metadata__.title, 41 | "severity": __rego_metadata__.severity, 42 | "type": __rego_metadata__.type, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /kubernetes/policies/pss/baseline/8_non_default_proc_masks_set_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV027 2 | 3 | test_proc_mount_is_set_denied { 4 | r := deny with input as { 5 | "apiVersion": "v1", 6 | "kind": "Pod", 7 | "metadata": {"name": "hello-proc-mount"}, 8 | "spec": {"containers": [{ 9 | "command": [ 10 | "sh", 11 | "-c", 12 | "echo 'Hello' && sleep 1h", 13 | ], 14 | "image": "busybox", 15 | "name": "hello", 16 | "ports": [{"hostPort": 8080}], 17 | "securityContext": {"procMount": "Unmasked"}, 18 | }]}, 19 | } 20 | 21 | count(r) == 1 22 | r[_].msg == "Pod 'hello-proc-mount' should not set 'spec.containers[*].securityContext.procMount' or 'spec.initContainers[*].securityContext.procMount'" 23 | } 24 | 25 | test_proc_mount_is_not_set_allowed { 26 | r := deny with input as { 27 | "apiVersion": "v1", 28 | "kind": "Pod", 29 | "metadata": {"name": "hello-proc-mount"}, 30 | "spec": {"containers": [{ 31 | "command": [ 32 | "sh", 33 | "-c", 34 | "echo 'Hello' && sleep 1h", 35 | ], 36 | "image": "busybox", 37 | "name": "hello", 38 | "ports": [{"hostPort": 8080}], 39 | "securityContext": {}, 40 | }]}, 41 | } 42 | 43 | count(r) == 0 44 | } 45 | -------------------------------------------------------------------------------- /kubernetes/policies/pss/baseline/9_unsafe_sysctl_options_set.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV026 2 | 3 | import data.lib.kubernetes 4 | import data.lib.utils 5 | 6 | default failSysctls = false 7 | 8 | __rego_metadata__ := { 9 | "id": "KSV026", 10 | "avd_id": "AVD-KSV-0026", 11 | "title": "Unsafe sysctl options set", 12 | "short_code": "no-unsafe-sysctl", 13 | "version": "v1.0.0", 14 | "severity": "MEDIUM", 15 | "type": "Kubernetes Security Check", 16 | "description": "Sysctls can disable security mechanisms or affect all containers on a host, and should be disallowed except for an allowed 'safe' subset. A sysctl is considered safe if it is namespaced in the container or the Pod, and it is isolated from other Pods or processes on the same Node.", 17 | "recommended_actions": "Do not set 'spec.securityContext.sysctls' or set to values in an allowed subset", 18 | "url": "https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline", 19 | } 20 | 21 | __rego_input__ := { 22 | "combine": false, 23 | "selector": [{"type": "kubernetes"}], 24 | } 25 | 26 | # Add allowed sysctls 27 | allowed_sysctls = { 28 | "kernel.shm_rmid_forced", 29 | "net.ipv4.ip_local_port_range", 30 | "net.ipv4.tcp_syncookies", 31 | "net.ipv4.ping_group_range", 32 | } 33 | 34 | # failSysctls is true if a disallowed sysctl is set 35 | failSysctls { 36 | pod := kubernetes.pods[_] 37 | set_sysctls := {sysctl | sysctl := pod.spec.securityContext.sysctls[_].name} 38 | sysctls_not_allowed := set_sysctls - allowed_sysctls 39 | count(sysctls_not_allowed) > 0 40 | } 41 | 42 | deny[res] { 43 | failSysctls 44 | 45 | msg := kubernetes.format(sprintf("%s '%s' should set 'securityContext.sysctl' to the allowed values", [kubernetes.kind, kubernetes.name])) 46 | 47 | res := { 48 | "msg": msg, 49 | "id": __rego_metadata__.id, 50 | "title": __rego_metadata__.title, 51 | "severity": __rego_metadata__.severity, 52 | "type": __rego_metadata__.type, 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /kubernetes/policies/pss/restricted/1_non_core_volume_types.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV028 2 | 3 | import data.lib.kubernetes 4 | import data.lib.utils 5 | 6 | __rego_metadata__ := { 7 | "id": "KSV028", 8 | "avd_id": "AVD-KSV-0028", 9 | "title": "Non-ephemeral volume types used", 10 | "short_code": "no-non-ephemeral-volumes", 11 | "version": "v1.0.0", 12 | "severity": "LOW", 13 | "type": "Kubernetes Security Check", 14 | "description": "In addition to restricting HostPath volumes, usage of non-ephemeral volume types should be limited to those defined through PersistentVolumes.", 15 | "recommended_actions": "Do not Set 'spec.volumes[*]' to any of the disallowed volume types.", 16 | "url": "https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted", 17 | } 18 | 19 | __rego_input__ := { 20 | "combine": false, 21 | "selector": [{"type": "kubernetes"}], 22 | } 23 | 24 | # Add disallowed volume type 25 | disallowed_volume_types = [ 26 | "gcePersistentDisk", 27 | "awsElasticBlockStore", 28 | # "hostPath", Baseline detects spec.volumes[*].hostPath 29 | "gitRepo", 30 | "nfs", 31 | "iscsi", 32 | "glusterfs", 33 | "rbd", 34 | "flexVolume", 35 | "cinder", 36 | "cephFS", 37 | "flocker", 38 | "fc", 39 | "azureFile", 40 | "vsphereVolume", 41 | "quobyte", 42 | "azureDisk", 43 | "portworxVolume", 44 | "scaleIO", 45 | "storageos", 46 | "csi", 47 | ] 48 | 49 | # getDisallowedVolumes returns a list of volume names 50 | # which set volume type to any of the disallowed volume types 51 | getDisallowedVolumes[name] { 52 | volume := kubernetes.volumes[_] 53 | type := disallowed_volume_types[_] 54 | utils.has_key(volume, type) 55 | name := volume.name 56 | } 57 | 58 | # failVolumeTypes is true if any of volume has a disallowed 59 | # volume type 60 | failVolumeTypes { 61 | count(getDisallowedVolumes) > 0 62 | } 63 | 64 | deny[res] { 65 | failVolumeTypes 66 | 67 | msg := kubernetes.format(sprintf("%s '%s' should set 'spec.volumes[*]' to type 'PersistentVolumeClaim'", [kubernetes.kind, kubernetes.name])) 68 | 69 | res := { 70 | "msg": msg, 71 | "id": __rego_metadata__.id, 72 | "title": __rego_metadata__.title, 73 | "severity": __rego_metadata__.severity, 74 | "type": __rego_metadata__.type, 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /kubernetes/policies/pss/restricted/1_non_core_volume_types_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV028 2 | 3 | test_disallowed_volume_type_used_denied { 4 | r := deny with input as { 5 | "apiVersion": "v1", 6 | "kind": "Pod", 7 | "metadata": {"name": "hello-volume-types"}, 8 | "spec": { 9 | "containers": [{ 10 | "command": [ 11 | "sh", 12 | "-c", 13 | "echo 'Hello' && sleep 1h", 14 | ], 15 | "image": "busybox", 16 | "name": "hello", 17 | }], 18 | "volumes": [{ 19 | "name": "volume-a", 20 | "scaleIO": { 21 | "gateway": "https://localhost:443/api", 22 | "system": "scaleio", 23 | "protectionDomain": "sd0", 24 | "storagePool": "sp1", 25 | "volumeName": "vol-a", 26 | "secretRef": {"name": "sio-secret"}, 27 | "fsType": "xfs", 28 | }, 29 | }], 30 | }, 31 | } 32 | 33 | count(r) == 1 34 | r[_].msg == "Pod 'hello-volume-types' should set 'spec.volumes[*]' to type 'PersistentVolumeClaim'" 35 | } 36 | 37 | test_no_volume_type_used_allowed { 38 | r := deny with input as { 39 | "apiVersion": "v1", 40 | "kind": "Pod", 41 | "metadata": {"name": "hello-volume-types"}, 42 | "spec": { 43 | "containers": [{ 44 | "command": [ 45 | "sh", 46 | "-c", 47 | "echo 'Hello' && sleep 1h", 48 | ], 49 | "image": "busybox", 50 | "name": "hello", 51 | }], 52 | "volumes": [{"name": "volume-a"}], 53 | }, 54 | } 55 | 56 | count(r) == 0 57 | } 58 | -------------------------------------------------------------------------------- /kubernetes/policies/pss/restricted/2_can_elevate_its_own_privileges.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV001 2 | 3 | import data.lib.kubernetes 4 | import data.lib.utils 5 | 6 | default checkAllowPrivilegeEscalation = false 7 | 8 | __rego_metadata__ := { 9 | "id": "KSV001", 10 | "avd_id": "AVD-KSV-0001", 11 | "title": "Process can elevate its own privileges", 12 | "short_code": "no-self-privesc", 13 | "version": "v1.0.0", 14 | "severity": "MEDIUM", 15 | "type": "Kubernetes Security Check", 16 | "description": "A program inside the container can elevate its own privileges and run as root, which might give the program control over the container and node.", 17 | "recommended_actions": "Set 'set containers[].securityContext.allowPrivilegeEscalation' to 'false'.", 18 | "url": "https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted", 19 | } 20 | 21 | __rego_input__ := { 22 | "combine": false, 23 | "selector": [{"type": "kubernetes"}], 24 | } 25 | 26 | # getNoPrivilegeEscalationContainers returns the names of all containers which have 27 | # securityContext.allowPrivilegeEscalation set to false. 28 | getNoPrivilegeEscalationContainers[container] { 29 | allContainers := kubernetes.containers[_] 30 | allContainers.securityContext.allowPrivilegeEscalation == false 31 | container := allContainers.name 32 | } 33 | 34 | # getPrivilegeEscalationContainers returns the names of all containers which have 35 | # securityContext.allowPrivilegeEscalation set to true or not set. 36 | getPrivilegeEscalationContainers[container] { 37 | container := kubernetes.containers[_].name 38 | not getNoPrivilegeEscalationContainers[container] 39 | } 40 | 41 | # checkAllowPrivilegeEscalation is true if any container has 42 | # securityContext.allowPrivilegeEscalation set to true or not set. 43 | checkAllowPrivilegeEscalation { 44 | count(getPrivilegeEscalationContainers) > 0 45 | } 46 | 47 | deny[res] { 48 | checkAllowPrivilegeEscalation 49 | 50 | msg := kubernetes.format(sprintf("Container '%s' of %s '%s' should set 'securityContext.allowPrivilegeEscalation' to false", [getPrivilegeEscalationContainers[_], kubernetes.kind, kubernetes.name])) 51 | 52 | res := { 53 | "msg": msg, 54 | "id": __rego_metadata__.id, 55 | "title": __rego_metadata__.title, 56 | "severity": __rego_metadata__.severity, 57 | "type": __rego_metadata__.type, 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /kubernetes/policies/pss/restricted/2_can_elevate_its_own_privileges_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV001 2 | 3 | test_allow_privilege_escalation_set_to_false_allowed { 4 | r := deny with input as { 5 | "apiVersion": "v1", 6 | "kind": "Pod", 7 | "metadata": {"name": "hello-privilege-escalation"}, 8 | "spec": {"containers": [{ 9 | "command": [ 10 | "sh", 11 | "-c", 12 | "echo 'Hello' && sleep 1h", 13 | ], 14 | "image": "busybox", 15 | "name": "hello", 16 | "securityContext": {"allowPrivilegeEscalation": false}, 17 | }]}, 18 | } 19 | 20 | count(r) == 0 21 | } 22 | 23 | test_allow_privilege_escalation_is_undefined_denied { 24 | r := deny with input as { 25 | "apiVersion": "v1", 26 | "kind": "Pod", 27 | "metadata": {"name": "hello-privilege-escalation"}, 28 | "spec": {"containers": [{ 29 | "command": [ 30 | "sh", 31 | "-c", 32 | "echo 'Hello' && sleep 1h", 33 | ], 34 | "image": "busybox", 35 | "name": "hello", 36 | }]}, 37 | } 38 | 39 | count(r) == 1 40 | r[_].msg == "Container 'hello' of Pod 'hello-privilege-escalation' should set 'securityContext.allowPrivilegeEscalation' to false" 41 | } 42 | 43 | test_allow_privilege_escalation_set_to_true_denied { 44 | r := deny with input as { 45 | "apiVersion": "v1", 46 | "kind": "Pod", 47 | "metadata": {"name": "hello-privilege-escalation"}, 48 | "spec": {"containers": [{ 49 | "command": [ 50 | "sh", 51 | "-c", 52 | "echo 'Hello' && sleep 1h", 53 | ], 54 | "image": "busybox", 55 | "name": "hello", 56 | "securityContext": {"allowPrivilegeEscalation": true}, 57 | }]}, 58 | } 59 | 60 | count(r) == 1 61 | r[_].msg == "Container 'hello' of Pod 'hello-privilege-escalation' should set 'securityContext.allowPrivilegeEscalation' to false" 62 | } 63 | -------------------------------------------------------------------------------- /kubernetes/policies/pss/restricted/3_runs_as_root.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV012 2 | 3 | import data.lib.kubernetes 4 | import data.lib.utils 5 | 6 | default checkRunAsNonRoot = false 7 | 8 | __rego_metadata__ := { 9 | "id": "KSV012", 10 | "avd_id": "AVD-KSV-0012", 11 | "title": "Runs as root user", 12 | "short_code": "no-root", 13 | "version": "v1.0.0", 14 | "severity": "MEDIUM", 15 | "type": "Kubernetes Security Check", 16 | "description": "'runAsNonRoot' forces the running image to run as a non-root user to ensure least privileges.", 17 | "recommended_actions": "Set 'containers[].securityContext.runAsNonRoot' to true.", 18 | "url": "https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted", 19 | } 20 | 21 | __rego_input__ := { 22 | "combine": false, 23 | "selector": [{"type": "kubernetes"}], 24 | } 25 | 26 | # getNonRootContainers returns the names of all containers which have 27 | # securityContext.runAsNonRoot set to true. 28 | getNonRootContainers[container] { 29 | allContainers := kubernetes.containers[_] 30 | allContainers.securityContext.runAsNonRoot == true 31 | container := allContainers.name 32 | } 33 | 34 | # getRootContainers returns the names of all containers which have 35 | # securityContext.runAsNonRoot set to false or not set. 36 | getRootContainers[container] { 37 | container := kubernetes.containers[_].name 38 | not getNonRootContainers[container] 39 | } 40 | 41 | # checkRunAsNonRoot is true if securityContext.runAsNonRoot is set to false 42 | # or if securityContext.runAsNonRoot is not set. 43 | checkRunAsNonRootContainers { 44 | count(getRootContainers) > 0 45 | } 46 | 47 | checkRunAsNonRootPod { 48 | allPods := kubernetes.pods[_] 49 | not allPods.spec.securityContext.runAsNonRoot 50 | } 51 | 52 | deny[res] { 53 | checkRunAsNonRootPod 54 | 55 | checkRunAsNonRootContainers 56 | 57 | msg := kubernetes.format(sprintf("Container '%s' of %s '%s' should set 'securityContext.runAsNonRoot' to true", [getRootContainers[_], kubernetes.kind, kubernetes.name])) 58 | 59 | res := { 60 | "msg": msg, 61 | "id": __rego_metadata__.id, 62 | "title": __rego_metadata__.title, 63 | "severity": __rego_metadata__.severity, 64 | "type": __rego_metadata__.type, 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /kubernetes/policies/pss/restricted/3_runs_as_root_test.rego: -------------------------------------------------------------------------------- 1 | package appshield.kubernetes.KSV012 2 | 3 | test_run_as_non_root_not_set_to_true_denied { 4 | r := deny with input as { 5 | "apiVersion": "v1", 6 | "kind": "Pod", 7 | "metadata": {"name": "hello-run-as-root"}, 8 | "spec": {"containers": [{ 9 | "command": [ 10 | "sh", 11 | "-c", 12 | "echo 'Hello' && sleep 1h", 13 | ], 14 | "image": "busybox", 15 | "name": "hello", 16 | }]}, 17 | } 18 | 19 | count(r) == 1 20 | r[_].msg == "Container 'hello' of Pod 'hello-run-as-root' should set 'securityContext.runAsNonRoot' to true" 21 | } 22 | 23 | test_run_as_non_root_not_set_to_true_for_all_containers_denied { 24 | r := deny with input as { 25 | "apiVersion": "v1", 26 | "kind": "Pod", 27 | "metadata": {"name": "hello-run-as-root"}, 28 | "spec": {"containers": [ 29 | { 30 | "command": [ 31 | "sh", 32 | "-c", 33 | "echo 'Hello' && sleep 1h", 34 | ], 35 | "image": "busybox", 36 | "name": "hello", 37 | "securityContext": {"runAsNonRoot": true}, 38 | }, 39 | { 40 | "command": [ 41 | "sh", 42 | "-c", 43 | "echo 'Hello' && sleep 1h", 44 | ], 45 | "image": "busybox", 46 | "name": "hello2", 47 | }, 48 | ]}, 49 | } 50 | 51 | count(r) == 1 52 | r[_].msg == "Container 'hello2' of Pod 'hello-run-as-root' should set 'securityContext.runAsNonRoot' to true" 53 | } 54 | 55 | test_run_as_non_root_set_to_true_for_pod_allowed { 56 | r := deny with input as { 57 | "apiVersion": "v1", 58 | "kind": "Pod", 59 | "metadata": {"name": "hello-run-as-root"}, 60 | "spec": { 61 | "securityContext": {"runAsNonRoot": true}, 62 | "containers": [{ 63 | "command": [ 64 | "sh", 65 | "-c", 66 | "echo 'Hello' && sleep 1h", 67 | ], 68 | "image": "busybox", 69 | "name": "hello", 70 | }], 71 | }, 72 | } 73 | 74 | count(r) == 0 75 | } 76 | 77 | test_run_as_non_root_set_to_true_for_container_allowed { 78 | r := deny with input as { 79 | "apiVersion": "v1", 80 | "kind": "Pod", 81 | "metadata": {"name": "hello-run-as-root"}, 82 | "spec": {"containers": [{ 83 | "command": [ 84 | "sh", 85 | "-c", 86 | "echo 'Hello' && sleep 1h", 87 | ], 88 | "image": "busybox", 89 | "name": "hello", 90 | "securityContext": {"runAsNonRoot": true}, 91 | }]}, 92 | } 93 | 94 | count(r) == 0 95 | } 96 | -------------------------------------------------------------------------------- /tools/avd_generator/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "text/template" 7 | 8 | "github.com/aquasecurity/appshield/tools/rego" 9 | ) 10 | 11 | func main() { 12 | regoFiles, err := rego.GetAllNonTestRegoFiles() 13 | if err != nil { 14 | fail("failed to get the rego files. %v", err) 15 | } 16 | 17 | var generateCount int 18 | for _, regoMeta := range regoFiles { 19 | 20 | if _, err := os.Stat(regoMeta.DocsFilePath()); err == nil { 21 | continue 22 | } 23 | 24 | if err := os.MkdirAll(regoMeta.DocsFolder(), 0755); err != nil { 25 | fail("an error occurred creating the docs folder for %s. %v", regoMeta.DocsFolder(), err) 26 | } 27 | 28 | writeDocsFile(regoMeta) 29 | generateCount++ 30 | } 31 | 32 | fmt.Printf("\nGenerated %d files in avd_docs\n", generateCount) 33 | } 34 | 35 | func writeDocsFile(meta *rego.RegoMetadata) { 36 | 37 | tmpl, err := template.New("appshield").Parse(docsMarkdownTemplate) 38 | if err != nil { 39 | fail("error occurred creating the template %v\n", err) 40 | } 41 | 42 | file, err := os.Create(meta.DocsFilePath()) 43 | if err != nil { 44 | fail("error occurred creating the file for %s", meta.DocsFilePath()) 45 | } 46 | 47 | if err := tmpl.Execute(file, meta); err != nil { 48 | fail("error occurred generating the document %v", err) 49 | } 50 | fmt.Printf("Generating file for policy %s\n", meta.Name) 51 | } 52 | 53 | func fail(msg string, args ...interface{}) { 54 | fmt.Printf(msg, args...) 55 | os.Exit(1) 56 | } 57 | 58 | var docsMarkdownTemplate = ` 59 | ### {{ .Title }} 60 | {{ .Description }} 61 | 62 | ### Impact 63 | 64 | 65 | 66 | {{ ` + "`{{ " + `remediationActions ` + "`}}" + `}} 67 | 68 | {{ if .Url }}### Links 69 | - {{ .Url }} 70 | {{ end }} 71 | ` 72 | -------------------------------------------------------------------------------- /tools/lint/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/aquasecurity/appshield/tools/rego" 9 | ) 10 | 11 | func main() { 12 | var failure bool 13 | regoFiles, err := rego.GetAllNonTestRegoFiles() 14 | if err != nil { 15 | fail("failed to get the rego files. %v", err) 16 | } 17 | 18 | fmt.Printf("\nRunning metadata linter against %d policies\n\n", len(regoFiles)) 19 | for _, regoMeta := range regoFiles { 20 | if valid, failures := regoMeta.Validate(); !valid { 21 | failure = true 22 | failureString := strings.Join(failures, "\n - ") 23 | fmt.Printf("Policy '%s' has invalid metadata: %s\n", regoMeta.Name, failureString) 24 | fmt.Println() 25 | } 26 | 27 | if !regoMeta.HasDocsMarkdown() { 28 | failure = true 29 | fmt.Printf(`Policy '%s' has no avd_docs, run "make generate_missing_docs" 30 | `, regoMeta.Name) 31 | } 32 | } 33 | if failure { 34 | fail("\nIssues were found, failing lint\n") 35 | } 36 | fmt.Println("No issues found") 37 | } 38 | 39 | func fail(msg string, args ...interface{}) { 40 | fmt.Printf(msg, args...) 41 | os.Exit(1) 42 | } 43 | -------------------------------------------------------------------------------- /tools/rego/rego_files.go: -------------------------------------------------------------------------------- 1 | package rego 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | ) 8 | 9 | func GetAllNonTestRegoFiles() ([]*RegoMetadata, error) { 10 | var regoFiles []*RegoMetadata 11 | 12 | if err := filepath.Walk("./", func(path string, info os.FileInfo, err error) error { 13 | if info.IsDir() || 14 | strings.HasSuffix(info.Name(), "_test.rego") || 15 | !strings.Contains(path, "/policies/") || 16 | filepath.Ext(path) != ".rego" { 17 | return nil 18 | } 19 | 20 | regoFile, err := NewRegoMetadata(path) 21 | if err != nil { 22 | return err 23 | } 24 | regoFiles = append(regoFiles, regoFile) 25 | 26 | return nil 27 | }); err != nil { 28 | return nil, err 29 | } 30 | 31 | return regoFiles, nil 32 | } 33 | --------------------------------------------------------------------------------