├── scripts ├── build-policies.sh └── ci-go-deps.sh ├── _config.yml ├── docs ├── images │ ├── konstraint-docs.png │ ├── pulling-policies.png │ ├── debugging-policies-trace.png │ ├── violation-markdown-example.png │ └── conftest-violation-output-json.png └── policies.md ├── exceptions ├── conftest-policy-packs.json ├── other-repo.json └── readme.md ├── policies ├── lib │ ├── packages_functions.rego │ ├── util_functions.rego │ └── docker_functions.rego ├── artifacthub-repo.yaml ├── terraform │ ├── required_tags │ │ ├── src_test.rego │ │ └── src.rego │ ├── encrypt_s3_buckets │ │ ├── src_test.rego │ │ └── src.rego │ ├── no_public_rds │ │ ├── src.rego │ │ └── src_test.rego │ ├── imdsv2_required │ │ ├── src_test.rego │ │ └── src.rego │ ├── artifacthub-pkg.yml │ └── block_public_acls_s3 │ │ ├── src.rego │ │ └── src_test.rego ├── docker │ ├── artifacthub-pkg.yml │ ├── sensitive_keys_in_env_args │ │ ├── src.rego │ │ └── src_test.rego │ └── deny_image_unless_from_registry │ │ ├── src.rego │ │ └── src_test.rego └── packages │ ├── artifacthub-pkg.yml │ ├── nodejs_package_must_use_org_scope │ ├── src_test.rego │ └── src.rego │ ├── nodejs_use_publishConfig │ ├── src.rego │ └── src_test.rego │ └── nodejs_must_use_recent_version │ ├── src.rego │ └── src_test.rego ├── .github ├── dependabot.yml ├── PULL_REQUEST_TEMPLATE │ └── rego_policy.md └── workflows │ └── ci.yml ├── data ├── terraform.yml ├── packages.yml └── docker.yml ├── .pre-commit-config.yaml ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── Makefile ├── LICENSE ├── README.md └── CONTRIBUTING.md /scripts/build-policies.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | make docs 3 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | name: Rally Health Conftest Policy Packs 2 | theme: jekyll-theme-cayman 3 | markdown: GFM 4 | -------------------------------------------------------------------------------- /docs/images/konstraint-docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rallyhealth/conftest-policy-packs/HEAD/docs/images/konstraint-docs.png -------------------------------------------------------------------------------- /docs/images/pulling-policies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rallyhealth/conftest-policy-packs/HEAD/docs/images/pulling-policies.png -------------------------------------------------------------------------------- /scripts/ci-go-deps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | go install github.com/plexsystems/konstraint@latest 4 | go install sigs.k8s.io/mdtoc@latest 5 | -------------------------------------------------------------------------------- /docs/images/debugging-policies-trace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rallyhealth/conftest-policy-packs/HEAD/docs/images/debugging-policies-trace.png -------------------------------------------------------------------------------- /docs/images/violation-markdown-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rallyhealth/conftest-policy-packs/HEAD/docs/images/violation-markdown-example.png -------------------------------------------------------------------------------- /docs/images/conftest-violation-output-json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rallyhealth/conftest-policy-packs/HEAD/docs/images/conftest-violation-output-json.png -------------------------------------------------------------------------------- /exceptions/conftest-policy-packs.json: -------------------------------------------------------------------------------- 1 | { 2 | "ALL": [ 3 | { 4 | "policy_id": "ALL", 5 | "jira_ticket": "RALLY-OOOO" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /policies/lib/packages_functions.rego: -------------------------------------------------------------------------------- 1 | package packages_functions 2 | 3 | import data.conftest 4 | 5 | is_package_json(resource) { 6 | conftest.file.name == "package.json" 7 | } 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | # Check for updates to GitHub Actions every weekday 8 | interval: "daily" 9 | -------------------------------------------------------------------------------- /policies/lib/util_functions.rego: -------------------------------------------------------------------------------- 1 | package util_functions 2 | 3 | has_key(x, k) { 4 | _ = x[k] 5 | } 6 | 7 | item_startswith_in_list(item, list) { 8 | some i 9 | list_item := list[i] 10 | startswith(item, list_item) 11 | } 12 | -------------------------------------------------------------------------------- /data/terraform.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # AWSSEC-0005 3 | # A list of strings representing required tags that must exist in Terraform AWS resources. 4 | minimum_required_tags: 5 | - owner_application 6 | - owner_domain 7 | - owner_team 8 | - owner_iam_role 9 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/rego_policy.md: -------------------------------------------------------------------------------- 1 | # Adding Rego Policy: RALLY-XXXX 2 | 3 | ## Description 4 | 5 | -------------------------------------------------------------------------------- /exceptions/other-repo.json: -------------------------------------------------------------------------------- 1 | { 2 | "__tests__/integration/files/test.dockerfile": [ 3 | { 4 | "policy_id": "CTNRSEC-0002", 5 | "jira_ticket": "JIRA-0001" 6 | }, 7 | { 8 | "policy_id": "CTNRSEC-0004", 9 | "jira_ticket": "JIRA-0002" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /policies/artifacthub-repo.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # from https://github.com/artifacthub/hub/blob/master/docs/metadata/artifacthub-repo.yml 3 | repositoryID: bd00adfa-5aee-4074-9cea-5c0735605d20 4 | owners: # (optional, used to claim repository ownership) 5 | - name: artis3n 6 | email: dev@artis3nal.com 7 | ignore: [] 8 | -------------------------------------------------------------------------------- /data/packages.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # PKGSEC-0001 3 | # One or more npm org scopes under which your internal packages must be published. 4 | # e.g. myorg/myapp 5 | approved_org_scopes: 6 | - myorg 7 | 8 | # PKGSEC-0003 9 | # One or more npm publishConfig registries to which packages may be published. 10 | approved_publishConfig_registries: 11 | - https://registry.npmjs.org 12 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/anderseknert/pre-commit-opa 3 | rev: v1.5.0 4 | hooks: 5 | - id: conftest-fmt 6 | - id: conftest-verify 7 | args: ['--data', 'data/', '--policy', 'policies/', '--output', 'github'] 8 | 9 | - repo: local 10 | hooks: 11 | - id: gen-policies 12 | name: Generate Documentation 13 | entry: scripts/build-policies.sh 14 | language: script 15 | files: policies|\.md 16 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.202.5/containers/debian/.devcontainer/base.Dockerfile 2 | 3 | # [Choice] Debian version (use bullseye or stretch on local arm64/Apple Silicon): bullseye, buster, stretch 4 | ARG VARIANT="buster" 5 | FROM mcr.microsoft.com/vscode/devcontainers/base:0-${VARIANT} 6 | 7 | # ** [Optional] Uncomment this section to install additional packages. ** 8 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 9 | # && apt-get -y install --no-install-recommends build-essential 10 | 11 | -------------------------------------------------------------------------------- /data/docker.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # CTNRSEC-0001 3 | # One or more approved registries from which container images may be downloaded. 4 | approved_private_registries: 5 | - 'my.private.registry' 6 | - 'other.private.registry' 7 | 8 | # CTNRSEC-0002 9 | # Deny any ARG or ENV key that appears to have a sensitive name, except for these known exclusions. 10 | # Amplitude, for example, was common at Rally for a period of time and Amplitude's API key is 11 | # meant to be bundled into the frontend, so it can be excluded from the policy. 12 | excepted_env_keys: 13 | # amplitude's api key is bundled into the frontend and is not considered a secret 14 | - amplitude 15 | -------------------------------------------------------------------------------- /policies/terraform/required_tags/src_test.rego: -------------------------------------------------------------------------------- 1 | package terraform_required_tags 2 | 3 | test_has_required_tags { 4 | count(warn) == 0 with input as {"resource": {"aws_security_group": {"sg-group-name": {"tags": { 5 | "owner_application": "application", 6 | "owner_domain": "domain", 7 | "owner_iam_role": "role", 8 | "owner_team": "team", 9 | }}}}} 10 | } 11 | 12 | test_missing_required_tags { 13 | count(warn) == 1 with input as {"resource": {"aws_security_group": {"sg-group-name": {"tags": {"Name": "MyName"}}}}} 14 | } 15 | 16 | test_no_tags { 17 | count(warn) == 0 with input as {"resource": {"aws_security_group": {"sg-group-name": {}}}} 18 | } 19 | -------------------------------------------------------------------------------- /policies/terraform/encrypt_s3_buckets/src_test.rego: -------------------------------------------------------------------------------- 1 | package terraform_encrypt_s3_buckets 2 | 3 | test_has_encryption { 4 | count(violation) == 0 with input as {"resource": {"aws_s3_bucket": {"encrypted-bucket": { 5 | "acl": "private", 6 | "bucket": "my-prod-bucket", 7 | "server_side_encryption_configuration": {"rule": {"apply_server_side_encryption_by_default": {"sse_algorithm": "AES256"}}}, 8 | }}}} 9 | } 10 | 11 | test_no_encryption { 12 | count(violation) == 1 with input as {"resource": {"aws_s3_bucket": {"unencrypted-bucket": { 13 | "acl": "private", 14 | "bucket": "my-prod-bucket", 15 | }}}} 16 | } 17 | 18 | test_not_s3_bucket { 19 | count(violation) == 0 with input as {"resource": {"aws_instance": {"fake-server": {}}}} 20 | } 21 | -------------------------------------------------------------------------------- /policies/lib/docker_functions.rego: -------------------------------------------------------------------------------- 1 | package docker_utils 2 | 3 | # FROM image is a stage found elsewhere in the Dockerfile 4 | is_a_multistage_build(baseInput, stage) { 5 | baseInput[x].Cmd == "from" 6 | val := baseInput[x].Value 7 | 8 | # Last position in FROM declaration is the name for this stage 9 | stageName := val[minus(count(val), 1)] 10 | 11 | # As long as the position is not the first and only thing 12 | # e.g. FROM image:latest 13 | # Looking for FROM image:latest AS myName 14 | stageName != val[0] 15 | 16 | # Loop through all such possible multi-stage builds, check if this stage comes from any of them 17 | startswith(stageName, stage) 18 | } 19 | 20 | is_a_variable(val) { 21 | startswith(val[0], "$") 22 | } 23 | 24 | from_scratch(base) { 25 | base == "scratch" 26 | } 27 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env make 2 | 3 | .PHONY: install 4 | install: 5 | brew install conftest golang jq pre-commit 6 | sh scripts/ci-go-deps.sh 7 | pre-commit install 8 | 9 | .PHONY: update 10 | update: 11 | brew upgrade conftest golang jq pre-commit 12 | go install github.com/plexsystems/konstraint@latest 13 | go install sigs.k8s.io/mdtoc@latest 14 | pre-commit autoupdate 15 | 16 | # Keep an eye on https://github.com/open-policy-agent/conftest/issues/518 for when coverage is supported 17 | .PHONY: test 18 | test: 19 | conftest verify --data data/ --policy policies/ 20 | 21 | .PHONY: fmt 22 | fmt: 23 | conftest fmt policies/ 24 | 25 | .PHONY: docs 26 | docs: 27 | $$(go env GOPATH)/bin/konstraint doc --output docs/policies.md --url https://github.com/RallyHealth/conftest-policy-packs 28 | $$(go env GOPATH)/bin/mdtoc --inplace README.md 29 | $$(go env GOPATH)/bin/mdtoc --inplace CONTRIBUTING.md 30 | 31 | -------------------------------------------------------------------------------- /policies/terraform/encrypt_s3_buckets/src.rego: -------------------------------------------------------------------------------- 1 | # @title Encrypt S3 Buckets 2 | # 3 | # S3 Buckets must have server-side encryption enabled. 4 | # See . 5 | # 6 | # While the security benefits of server-side bucket encryption are nebulous given practical threat scenarios, 7 | # those wishing to apply such a control may do so with this policy. 8 | # You may also be required to enforce this as a compliance checkbox. 9 | package terraform_encrypt_s3_buckets 10 | 11 | import data.util_functions 12 | 13 | policyID := "AWSSEC-0001" 14 | 15 | violation[{"policyId": policyID, "msg": msg}] { 16 | resource := input.resource.aws_s3_bucket 17 | a_resource := resource[name] 18 | not util_functions.has_key(a_resource, "server_side_encryption_configuration") 19 | 20 | msg := sprintf("Missing S3 encryption for `%s`. Required flag: `server_side_encryption_configuration`", [name]) 21 | } 22 | -------------------------------------------------------------------------------- /policies/terraform/no_public_rds/src.rego: -------------------------------------------------------------------------------- 1 | # @title RDS Instances May Not Be Public 2 | # 3 | # RDS instances must block public access. 4 | # The `publicly_accessible` attribute, if defined, must be set to `false`. 5 | # The attribute is `false` by default if not specified. 6 | # 7 | # See . 8 | package terraform_no_public_rds 9 | 10 | import data.util_functions 11 | 12 | policyID := "AWSSEC-0003" 13 | 14 | has_public_attribute(resource) { 15 | util_functions.has_key(resource, "publicly_accessible") 16 | } 17 | 18 | violation[{"policyId": policyID, "msg": msg}] { 19 | resource := input.resource.aws_db_instance 20 | a_resource := resource[name] 21 | has_public_attribute(a_resource) 22 | a_resource.publicly_accessible != false 23 | 24 | msg := sprintf("RDS instances must not be publicly exposed. Set `publicly_accessible` to `false` on aws_db_instance.`%s`", [name]) 25 | } 26 | -------------------------------------------------------------------------------- /policies/terraform/no_public_rds/src_test.rego: -------------------------------------------------------------------------------- 1 | package terraform_no_public_rds 2 | 3 | test_is_not_public { 4 | count(violation) == 0 with input as {"resource": {"aws_db_instance": {"encrypted-db": { 5 | "allocated_storage": 10, 6 | "engine": "mysql", 7 | "engine_version": "5.7", 8 | "instance_class": "db.t3.micro", 9 | "name": "mydb", 10 | }}}} 11 | } 12 | 13 | test_is_public { 14 | count(violation) == 1 with input as {"resource": {"aws_db_instance": {"encrypted-db": { 15 | "allocated_storage": 10, 16 | "engine": "mysql", 17 | "engine_version": "5.7", 18 | "instance_class": "db.t3.micro", 19 | "name": "mydb", 20 | "publicly_accessible": true, 21 | }}}} 22 | } 23 | 24 | test_explicitly_is_not_public { 25 | count(violation) == 0 with input as {"resource": {"aws_db_instance": {"encrypted-db": { 26 | "allocated_storage": 10, 27 | "engine": "mysql", 28 | "engine_version": "5.7", 29 | "instance_class": "db.t3.micro", 30 | "name": "mydb", 31 | "publicly_accessible": false, 32 | }}}} 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Rally Health, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /policies/docker/artifacthub-pkg.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # from https://github.com/artifacthub/hub/blob/master/docs/metadata/artifacthub-pkg.yml 3 | version: 1.0.2 4 | name: docker-policies 5 | displayName: OPA Conftest Docker Policies 6 | createdAt: 2021-09-10T21:27:55.549418+00:00 7 | description: OPA Conftest policies enforcing security standards against Dockerfiles. 8 | readme: | 9 | OPA Conftest policies enforcing security standards against Dockerfiles. 10 | 11 | The following policies are defined: 12 | 13 | - `CTNRSEC-0001`: Dockerfiles Must Pull From An Approved Private Registry 14 | - `CTNRSEC-0002`: Dockerfiles Should Not Use Environment Variables For Sensitive Values 15 | install: | 16 | `conftest pull git::https://github.com/rallyhealth/conftest-policy-packs.git//policies` 17 | 18 | Configure your org-specific `--data` as necessary from [data/](https://github.com/rallyhealth/conftest-policy-packs/tree/main/data) 19 | 20 | Run your conftest command: `conftest verify --data data/ --policy policies/` 21 | homeURL: https://rallyhealth.github.io/conftest-policy-packs/ 22 | keywords: 23 | - opa 24 | - conftest 25 | - docker 26 | license: MIT 27 | provider: 28 | name: Rally Health 29 | -------------------------------------------------------------------------------- /policies/packages/artifacthub-pkg.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # from https://github.com/artifacthub/hub/blob/master/docs/metadata/artifacthub-pkg.yml 3 | version: 1.1.0 4 | name: coding-lang-package-policies 5 | displayName: OPA Conftest Lang Package Policies 6 | createdAt: 2021-09-10T21:27:55.549418+00:00 7 | description: OPA Conftest policies enforcing security standards against coding language package files, e.g. package.json for NodeJS. 8 | readme: | 9 | These policies enforce security standards against coding language package files, e.g. `package.json` for NodeJS. 10 | 11 | The following policies are defined: 12 | 13 | - `PKGSEC-0001`: NPM Packages Must Be Published Under An Organization Scope 14 | - `PKGSEC-0002`: NodeJS Projects Must Use A Recent NodeJS Version 15 | - `PKGSEC-0003`: NPM Packages Must Be Published To Approved Registry 16 | install: | 17 | `conftest pull git::https://github.com/rallyhealth/conftest-policy-packs.git//policies` 18 | 19 | Configure your org-specific `--data` as necessary from [data/](https://github.com/rallyhealth/conftest-policy-packs/tree/main/data) 20 | 21 | Run your conftest command: `conftest verify --data data/ --policy policies/` 22 | homeURL: https://rallyhealth.github.io/conftest-policy-packs/ 23 | keywords: 24 | - opa 25 | - conftest 26 | - nodejs 27 | license: MIT 28 | provider: 29 | name: Rally Health 30 | -------------------------------------------------------------------------------- /policies/terraform/required_tags/src.rego: -------------------------------------------------------------------------------- 1 | # @title Resources Must Use Required Tags 2 | # 3 | # AWS resources should be tagged with a minimum set of organization tags for logistical purposes. 4 | # 5 | # As this policy is executing on Terraform source code, this policy is reported as a "warn" instead of a "violation." 6 | # This policy is best evaluated against a Terraform plan, in which case the policy may need to be adapted to correctly parse a resource in a plan file. 7 | # 8 | package terraform_required_tags 9 | 10 | import data.minimum_required_tags 11 | 12 | policyID := "AWSSEC-0005" 13 | 14 | tags_contain_proper_keys(tags) { 15 | keys := {key | tags[key]} 16 | minimum_tags_set := {x | x := minimum_required_tags[i]} 17 | leftover := minimum_tags_set - keys 18 | 19 | # If all minimum_tags exist in keys, the leftover set should be empty - equal to a new set() 20 | leftover == set() 21 | } 22 | 23 | warn[msg] { 24 | resource := input.resource[resource_type] 25 | tags := resource[name].tags 26 | 27 | # Create an array of resources, only if they are missing the minimum tags 28 | resources := [sprintf("%v.%v", [resource_type, name]) | not tags_contain_proper_keys(tags)] 29 | 30 | resources != [] 31 | msg := sprintf("%s: Invalid tags (missing minimum required tags) for the following resource(s): `%v`. Required tags: `%v`", [policyID, resources, minimum_required_tags]) 32 | } 33 | -------------------------------------------------------------------------------- /policies/terraform/imdsv2_required/src_test.rego: -------------------------------------------------------------------------------- 1 | package terraform_ec2_imdsv2_required 2 | 3 | test_has_metadata_v2_required { 4 | count(violation) == 0 with input as {"resource": {"aws_instance": {"web": { 5 | "ami": "data.aws_ami.ubuntu.id", 6 | "instance_type": "t3.micro", 7 | "metadata_options": {"http_tokens": "required"}, 8 | }}}} 9 | } 10 | 11 | test_has_metadata_v2_optional { 12 | count(violation) == 1 with input as {"resource": {"aws_instance": {"web": { 13 | "ami": "data.aws_ami.ubuntu.id", 14 | "instance_type": "t3.micro", 15 | "metadata_options": {"http_tokens": "optional"}, 16 | }}}} 17 | } 18 | 19 | test_has_metadata_v2_no_http_token { 20 | count(violation) == 1 with input as {"resource": {"aws_instance": {"web": { 21 | "ami": "data.aws_ami.ubuntu.id", 22 | "instance_type": "t3.micro", 23 | "metadata_options": {"http_endpoint": "enabled"}, 24 | }}}} 25 | } 26 | 27 | test_no_metadata_v2 { 28 | count(violation) == 1 with input as {"resource": {"aws_instance": {"web": { 29 | "ami": "data.aws_ami.ubuntu.id", 30 | "instance_type": "t3.micro", 31 | }}}} 32 | } 33 | 34 | test_not_aws_instance { 35 | count(violation) == 0 with input as {"resource": {"aws_s3_bucket": {"encrypted-bucket": { 36 | "acl": "private", 37 | "bucket": "my-test-bucket", 38 | "server_side_encryption_configuration": {"rule": {"apply_server_side_encryption_by_default": {"sse_algorithm": "AES256"}}}, 39 | }}}} 40 | } 41 | -------------------------------------------------------------------------------- /policies/terraform/artifacthub-pkg.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # from https://github.com/artifacthub/hub/blob/master/docs/metadata/artifacthub-pkg.yml 3 | version: 1.1.0 4 | name: terraform-policies 5 | displayName: OPA Conftest Terraform Policies 6 | createdAt: 2021-09-10T21:27:55.549418+00:00 7 | description: OPA Conftest policies enforcing security standards against Terraform. 8 | readme: | 9 | OPA Conftest policies enforcing security standards against Terraform. 10 | 11 | Notably, these policies support Terraform source code vs. rendered plan JSON, for CI support early in the developer workflow. 12 | 13 | The following policies are defined: 14 | 15 | - `AWSSEC-0001`: Encrypt S3 Buckets 16 | - `AWSSEC-0002`: EC2 Instances Must Use Instance Metadata Service Version 2 17 | - `AWSSEC-0003`: RDS Instances May Not Be Public 18 | - `AWSSEC-0004`: Block Public Access of S3 Buckets 19 | - `AWSSEC-0005`: Resources Must Use Required Tags 20 | install: | 21 | `conftest pull git::https://github.com/rallyhealth/conftest-policy-packs.git//policies` 22 | 23 | Configure your org-specific `--data` as necessary from [data/](https://github.com/rallyhealth/conftest-policy-packs/tree/main/data) 24 | 25 | Run your conftest command: `conftest verify --data data/ --policy policies/` 26 | homeURL: https://rallyhealth.github.io/conftest-policy-packs/ 27 | keywords: 28 | - opa 29 | - conftest 30 | - terraform 31 | license: MIT 32 | provider: 33 | name: Rally Health 34 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.202.5/containers/debian 3 | { 4 | "name": "Debian", 5 | "runArgs": ["--init"], 6 | "build": { 7 | "dockerfile": "Dockerfile", 8 | // Update 'VARIANT' to pick an Debian version: bullseye, buster, stretch 9 | // Use bullseye or stretch on local arm64/Apple Silicon. 10 | "args": { "VARIANT": "bullseye" } 11 | }, 12 | 13 | // Set *default* container specific settings.json values on container create. 14 | "settings": {}, 15 | 16 | // Add the IDs of extensions you want installed when the container is created. 17 | "extensions": [ 18 | "golang.Go", 19 | "MS-vsliveshare.vsliveshare", 20 | "yzhang.markdown-all-in-one", 21 | "tsandall.opa" 22 | ], 23 | 24 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 25 | // "forwardPorts": [], 26 | 27 | // Uncomment to use the Docker CLI from inside the container. See https://aka.ms/vscode-remote/samples/docker-from-docker. 28 | // "mounts": [ "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind" ], 29 | 30 | // Uncomment when using a ptrace-based debugger like C++, Go, and Rust 31 | // "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ], 32 | 33 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 34 | "remoteUser": "vscode", 35 | "features": { 36 | "git": "latest", 37 | "github-cli": "latest", 38 | "homebrew": "latest", 39 | "golang": "latest" 40 | }, 41 | "onCreateCommand": "make install" 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Conftest CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | test: 10 | name: 'Pre-Commit Checks' 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2.4.0 14 | 15 | # Pre-req for the pre-commit action 16 | - uses: actions/setup-python@v2.2.2 17 | with: 18 | python-version: '3.9' 19 | 20 | - uses: actions/setup-go@v2.1.4 21 | with: 22 | go-version: '^1.16' 23 | 24 | - name: Set up Homebrew 25 | id: set-up-homebrew 26 | uses: Homebrew/actions/setup-homebrew@master 27 | 28 | # If the version of konstraint updates during the lifetime of an open PR, the 29 | # caching benefits will disappear. 30 | # In the general case, this cuts ~3 minutes from this job. 31 | - name: 'Cache Go' 32 | uses: actions/cache@v2 33 | with: 34 | path: | 35 | ~/.cache/go-build 36 | ~/go/ 37 | key: ${{ runner.os }}-go-${{ github.ref }} 38 | restore-keys: | 39 | ${{ runner.os }}-go- 40 | 41 | - name: 'Install Dependencies' 42 | run: | 43 | brew install conftest 44 | ./scripts/ci-go-deps.sh 45 | 46 | - name: 'Pre-Commit Checks' 47 | uses: pre-commit/action@v2.0.3 48 | 49 | - name: "Ensure data files are correct file type" 50 | run: | 51 | # This will exit 1 if any files are found with a different file extension 52 | if [[ $(find ./data -maxdepth 1 -type f -not -name "*.yml") != "" ]]; 53 | then 54 | echo "::error::Some files in data/ do not have .yml extensions" 55 | exit 1 56 | fi 57 | -------------------------------------------------------------------------------- /exceptions/readme.md: -------------------------------------------------------------------------------- 1 | # Policy Exceptions 2 | 3 | We have opted to handle exceptions in the code surrounding Conftest execution, although as JSON files you may opt to modify your Rego to accommodate them. 4 | 5 | Our ECS task runs Conftest with the policies in this repo and records violations by file per repo. It then compares the listed violations to a map constructed from these policy exceptions. 6 | If a match is found, that violation is excluded from the results published as a GitHub status check for developers. 7 | It is recorded as an exempted violation in the Datadog dashboard providing the holistic landscape for the Appsec team. 8 | 9 | Given that framework, exceptions are organized by repository in our organization. 10 | Per repo, exceptions are granted on a file level. 11 | We did not need to go more granular, and producing more granular exceptions requires more complexity in the custom code surrounding the Conftest evaluation. 12 | Please open source your exception frameworks to share with the community if you follow a different approach. 13 | 14 | An exemption is granted for specific `policyID`s as documented in our [policies](/CONTRIBUTING.md##policy-id). 15 | As we handle exceptions with our custom logic, we also support a catch-all `ALL` exception that can be applied to a file in a repo or for the entire repo. 16 | An example is provided in `conftest-policy-packs.json`. 17 | A more realistic example of our exceptions can be found in `other-repo.json`. 18 | 19 | Exceptions must be approved by Security, in the form of a Jira ticket. 20 | To grant an exception, a dev team files a Jira ticket with a brief justification and opens a PR in this repo to add the specific policy exemption to their repo. 21 | Once that exception is approved the Appsec team merges the PR and the next commit anywhere in the organization will pick up the latest changes. 22 | -------------------------------------------------------------------------------- /policies/packages/nodejs_package_must_use_org_scope/src_test.rego: -------------------------------------------------------------------------------- 1 | package nodejs_package_must_have_org_scope 2 | 3 | mockConftestData := { 4 | "MOCKED": true, 5 | "file": { 6 | "dir": "/Users/testuser/Documents/conftest-policy-packs", 7 | "name": "package.json", 8 | }, 9 | } 10 | 11 | test_org_scope { 12 | count(violation) == 0 with input as { 13 | "author": "Test author", 14 | "bugs": {"url": "https://github.com/rallyhealth/conftest-policy-packs/issues"}, 15 | "description": "Test scope", 16 | "homepage": "https://github.com/rallyhealth/conftest-policy-packs#readme", 17 | "license": "ISC", 18 | "main": "index.js", 19 | "name": "@myorg/myapp", 20 | "repository": { 21 | "type": "git", 22 | "url": "git+git@github.com:rallyhealth/conftest-policy-packs.git", 23 | }, 24 | "scripts": {"test": "jest"}, 25 | "version": "1.0.0", 26 | } 27 | with data.conftest as mockConftestData 28 | } 29 | 30 | test_missing_scope { 31 | count(violation) == 1 with input as { 32 | "author": "Test author", 33 | "bugs": {"url": "https://github.com/rallyhealth/conftest-policy-packs/issues"}, 34 | "description": "Test scope", 35 | "homepage": "https://github.com/rallyhealth/conftest-policy-packs#readme", 36 | "license": "ISC", 37 | "main": "index.js", 38 | "name": "myapp", 39 | "repository": { 40 | "type": "git", 41 | "url": "git+git@github.com:rallyhealth/conftest-policy-packs.git", 42 | }, 43 | "scripts": {"test": "jest"}, 44 | "version": "1.0.0", 45 | } 46 | with data.conftest as mockConftestData 47 | } 48 | 49 | test_wrong_scope { 50 | count(violation) == 1 with input as { 51 | "author": "Test author", 52 | "bugs": {"url": "https://github.com/rallyhealth/conftest-policy-packs/issues"}, 53 | "description": "Test scope", 54 | "homepage": "https://github.com/rallyhealth/conftest-policy-packs#readme", 55 | "license": "ISC", 56 | "main": "index.js", 57 | "name": "@wrongorg/myapp", 58 | "repository": { 59 | "type": "git", 60 | "url": "git+git@github.com:rallyhealth/conftest-policy-packs.git", 61 | }, 62 | "scripts": {"test": "jest"}, 63 | "version": "1.0.0", 64 | } 65 | with data.conftest as mockConftestData 66 | } 67 | -------------------------------------------------------------------------------- /policies/packages/nodejs_package_must_use_org_scope/src.rego: -------------------------------------------------------------------------------- 1 | # @title NPM Packages Must Be Published Under An Organization Scope 2 | # 3 | # NodeJS packages are subject to typosquatting, in which a malicious package is published 4 | # with a slight misspelling. The aim is to infect end users who misspell your package. 5 | # While this relies on end user misconfiguration, package owners can still take steps to reduce the viability 6 | # of such a mistake. 7 | # 8 | # This can be avoided by scoping an organizational package beneath an [organization scope](https://docs.npmjs.com/cli/v7/using-npm/scope). 9 | # Organizations publishing packages to a private registry can use a private organization scope. 10 | # 11 | # Scopes are a way of grouping related packages together, and also affect a few things about the way npm treats the package. 12 | # Each npm user/organization has their own scope, and only you can add packages in your scope. 13 | # This means you don't have to worry about someone taking your package name ahead of you. 14 | # Thus it is also a good way to signal official packages for organizations. 15 | package nodejs_package_must_have_org_scope 16 | 17 | import data.approved_org_scopes 18 | import data.packages_functions 19 | import data.util_functions 20 | 21 | policyID := "PKGSEC-0001" 22 | 23 | has_org_scope(name) { 24 | startswith(name, "@") 25 | } 26 | 27 | violation[{"policyId": policyID, "msg": msg}] { 28 | packages_functions.is_package_json(input) 29 | package_name := input.name 30 | not has_org_scope(package_name) 31 | msg := sprintf("NPM packages must be wrapped beneath an organization scope (e.g. `@orgscope/mypackage`). `%s` does not use any organization scope. Approved scopes are: `%v`.", [package_name, approved_org_scopes]) 32 | } 33 | 34 | violation[{"policyId": policyID, "msg": msg}] { 35 | packages_functions.is_package_json(input) 36 | package_name := input.name 37 | has_org_scope(package_name) 38 | org_name := substring(package_name, 1, -1) 39 | not util_functions.item_startswith_in_list(org_name, approved_org_scopes) 40 | msg := sprintf("NodeJS packages must be wrapped beneath an organization scope (e.g. `@orgscope/mypackage`). `%s` does not use an approved organization scope. Approved scopes are: `%v`.", [package_name, approved_org_scopes]) 41 | } 42 | -------------------------------------------------------------------------------- /policies/packages/nodejs_use_publishConfig/src.rego: -------------------------------------------------------------------------------- 1 | # @title NPM Packages Must Be Published To Approved Registry 2 | # 3 | # NodeJS packages with a `publishConfig` object must have the `registry` field set to an approved organizational registry. 4 | # 5 | # For more information about the `registry` field of the `publishConfig` object, see . 6 | package nodejs_package_must_use_org_publish_config 7 | 8 | import data.approved_publishConfig_registries 9 | import data.packages_functions 10 | import data.util_functions 11 | 12 | policyID := "PKGSEC-0003" 13 | 14 | approved_registries := {registry_name | registry_name := approved_publishConfig_registries[i]} 15 | 16 | # If publishConfig's registry field is not the correct URL 17 | violation[{"policyId": policyID, "msg": msg}] { 18 | packages_functions.is_package_json(input) 19 | util_functions.has_key(input, "publishConfig") 20 | 21 | publish_config := input.publishConfig 22 | util_functions.has_key(publish_config, "registry") 23 | 24 | not approved_registries[publish_config.registry] 25 | 26 | msg := sprintf("NPM packages must have a `publishConfig` field set to an approved registry. An unapproved registry is listed. Approved registries are: `%v`.", [approved_publishConfig_registries]) 27 | } 28 | 29 | # if publishConfig does not have a registry field 30 | violation[{"policyId": policyID, "msg": msg}] { 31 | packages_functions.is_package_json(input) 32 | util_functions.has_key(input, "publishConfig") 33 | 34 | publish_config := input.publishConfig 35 | not util_functions.has_key(publish_config, "registry") 36 | 37 | msg := sprintf("NPM packages must have a `publishConfig` field set to an approved registry. No `registry` is set. Approved registries are: `%v`.", [approved_publishConfig_registries]) 38 | } 39 | 40 | # if publishConfig is not set, defaulting to public NPM registry 41 | violation[{"policyId": policyID, "msg": msg}] { 42 | packages_functions.is_package_json(input) 43 | not util_functions.has_key(input, "publishConfig") 44 | 45 | msg := sprintf("NPM packages must have a `publishConfig` field set to an approved registry. No `publishConfig` is set. Approved registries are: `%v`.", [approved_publishConfig_registries]) 46 | } 47 | -------------------------------------------------------------------------------- /policies/docker/sensitive_keys_in_env_args/src.rego: -------------------------------------------------------------------------------- 1 | # @title Dockerfiles Should Not Use Environment Variables For Sensitive Values 2 | # 3 | # Docker images should not pass sensitive values through `ENV` or `ARG` variables. 4 | # This binds the secret value into a layer of the Docker image and makes the secret 5 | # recoverable by anyone with access to the final built image through the `docker history` command. 6 | # 7 | # Instead, users should use the [Buildkit --secret](https://docs.docker.com/develop/develop-images/build_enhancements/#new-docker-build-secret-information) 8 | # flag or a multi-stage build. 9 | # 10 | # If you use a multi-stage build, an `ARG` or `ENV` with a sensitive value **must** not exist in the final built image. 11 | package sensitive_keys_in_env_args 12 | 13 | import data.docker_utils 14 | import data.excepted_env_keys 15 | 16 | policyID := "CTNRSEC-0002" 17 | 18 | sensitive_env_keys = [ 19 | "secret", 20 | "apikey", 21 | "token", 22 | "passwd", 23 | "password", 24 | "pwd", 25 | "api_key", 26 | "credential", 27 | ] 28 | 29 | cmds := ["env", "arg"] 30 | 31 | violation[{"policyId": policyID, "msg": msg}] { 32 | # Get all indices where cmd is 'from' 33 | from_stmt_indices := [index | input[i].Cmd == "from"; index := i] 34 | from_index := from_stmt_indices[x] 35 | 36 | # from_val is an array like ["my.private.registry/ubuntu:20.04", "AS", "builder"] 37 | from_val := input[from_index].Value 38 | not docker_utils.is_a_multistage_build(input, from_val[0]) 39 | 40 | # We only care about evaluating 'env' statements that correspond to the final 'from' statement 41 | start := from_stmt_indices[minus(count(from_stmt_indices), 1)] 42 | end := count(input) 43 | final_from_slice := array.slice(input, start, end) 44 | 45 | cmd := cmds[_] 46 | final_from_slice[j].Cmd == cmd 47 | val := final_from_slice[j].Value 48 | sensitive_key := sensitive_env_keys[_] 49 | excepted_key := excepted_env_keys[_] 50 | contains(lower(val[0]), sensitive_key) 51 | not contains(lower(val[0]), excepted_key) 52 | 53 | msg := sprintf("A %s key [`%s`] was found in this Dockerfile that suggets you are storing a sensitive value in a layer of your Docker image. Dockerfiles should instead use the [Buildkit --secret](https://docs.docker.com/develop/develop-images/build_enhancements/#new-docker-build-secret-information) flag or place the sensitive value in an earlier stage of a multi-stage build.", [upper(cmd), val[0]]) 54 | } 55 | -------------------------------------------------------------------------------- /policies/docker/deny_image_unless_from_registry/src.rego: -------------------------------------------------------------------------------- 1 | # @title Dockerfiles Must Pull From An Approved Private Registry 2 | # 3 | # Dockerfiles must pull images from an approved private registry and not from public repositories. 4 | # The FROM statement must have a private registry prepended, e.g. "our.private.registry/..." as the value. 5 | package docker_pull_from_registry 6 | 7 | import data.approved_private_registries 8 | import data.docker_utils 9 | import data.util_functions 10 | 11 | policyID := "CTNRSEC-0001" 12 | 13 | cmds := ["env", "arg"] 14 | 15 | violation[{"policyId": policyID, "msg": msg}] { 16 | input[i].Cmd == "from" 17 | val := input[i].Value 18 | not docker_utils.is_a_variable(val) 19 | not docker_utils.is_a_multistage_build(input, val[0]) 20 | not docker_utils.from_scratch(val[0]) 21 | 22 | not util_functions.item_startswith_in_list(val[0], approved_private_registries) 23 | msg := sprintf("Dockerfiles must pull images from an approved private registry (`FROM my.private.registry/...`). The image `%s` does not pull from an approved private registry. The following are approved registries: `%v`.", [val[0], approved_private_registries]) 24 | } 25 | 26 | # FROM check where a variable is used for the image 27 | violation[{"policyId": policyID, "msg": msg}] { 28 | input[i].Cmd == "from" 29 | val := input[i].Value 30 | not docker_utils.is_a_multistage_build(input, val[0]) 31 | docker_utils.is_a_variable(val) 32 | 33 | # Get variable name without the $ 34 | variableNameWithVersion := substring(val[0], 1, -1) 35 | 36 | # Drop the version, if present, to make it easier to find the right variable 37 | variableName := split(variableNameWithVersion, ":")[0] 38 | 39 | input[j].Cmd == cmds[_] 40 | argCmd := input[j].Value[0] 41 | 42 | # ARG or ENV is a match for the variable we're looking for 43 | startswith(argCmd, variableName) 44 | 45 | # Grab the value of the ARGument or ENV var 46 | # e.g. ARG MYIMAGE=ubuntu:latest => ubuntu:latest 47 | argNameAndValue := split(argCmd, "=") 48 | imageInArg := trim(argNameAndValue[1], "\"") 49 | 50 | not util_functions.item_startswith_in_list(imageInArg, approved_private_registries) 51 | msg := sprintf("Dockerfiles must pull images from an approved private registry (`FROM my.private.registry/...`). The image `%s` in variable `%s` does not pull from an approved private registry. The following are approved registries: `%v`.", [imageInArg, argNameAndValue[0], approved_private_registries]) 52 | } 53 | -------------------------------------------------------------------------------- /policies/terraform/imdsv2_required/src.rego: -------------------------------------------------------------------------------- 1 | # @title EC2 Instances Must Use Instance Metadata Service Version 2 2 | # 3 | # EC2 instances must use instance metadata service version 2 (IMDSv2) to prevent 4 | # [server-side request forgery (SSRF)](https://portswigger.net/web-security/ssrf) attacks. 5 | # 6 | # Set `http_tokens` to `required` in the 7 | # [metadata-options](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance#metadata-options). 8 | # 9 | # AWS released v2 of the instance metadata service as a response to the 2019 Capital One breach. 10 | # IMDSv2 helps prevent SSRF from being executed against instance metadata, preventing attackers 11 | # from stealing instance credentials via a vulnerability in a web server application. 12 | # 13 | # IMDSv2 adds a session token in the `X-aws-ec2-metadata-token` header that must be present to retrieve any 14 | # information from instance metadata. 15 | # This occurs automatically for systems using the AWS CLI. 16 | # Systems making direct `curl` requests to instance metadata must modify their requests to the following format: 17 | # 18 | # ```bash 19 | # # Get a token with a 60-second lifetime 20 | # TOKEN=`curl -X PUT "http://196.254.196.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 60"` 21 | # # Make instance metadata request 22 | # curl http://169.254.169.254/latest/meta-data/profile -H "X-aws-ec2-metadata-token: $TOKEN" 23 | # ``` 24 | package terraform_ec2_imdsv2_required 25 | 26 | import data.util_functions 27 | 28 | policyID := "AWSSEC-0002" 29 | 30 | violation[{"policyId": policyID, "msg": msg}] { 31 | resource := input.resource.aws_instance 32 | aws_resource := resource[resource_name] 33 | 34 | # Check for metadata options 35 | util_functions.has_key(aws_resource, "metadata_options") 36 | metadata := aws_resource.metadata_options 37 | 38 | # Check for http_tokens and correct value 39 | util_functions.has_key(metadata, "http_tokens") 40 | metadata.http_tokens != "required" 41 | 42 | msg := sprintf("Instance metadata version 2 not enabled for resource `aws_instance.%s`. Add a `metadata_options` block with `http_tokens` set to `required`.", [resource_name]) 43 | } 44 | 45 | violation[{"policyId": policyID, "msg": msg}] { 46 | resource := input.resource.aws_instance 47 | aws_resource := resource[resource_name] 48 | 49 | # Check for metadata options 50 | util_functions.has_key(aws_resource, "metadata_options") 51 | metadata := aws_resource.metadata_options 52 | 53 | # If no http_tokens field, flag it 54 | not util_functions.has_key(metadata, "http_tokens") 55 | 56 | msg := sprintf("Instance metadata version 2 not enabled for resource `aws_instance.%s`. Add a `metadata_options` block with `http_tokens` set to `required`.", [resource_name]) 57 | } 58 | 59 | violation[{"policyId": policyID, "msg": msg}] { 60 | resource := input.resource.aws_instance 61 | aws_resource := resource[resource_name] 62 | 63 | # Check for metadata_options 64 | not util_functions.has_key(aws_resource, "metadata_options") 65 | 66 | msg := sprintf("Instance metadata version 2 not enabled for resource `aws_instance.%s`. Add a `metadata_options` block with `http_tokens` set to `required`.", [resource_name]) 67 | } 68 | -------------------------------------------------------------------------------- /policies/terraform/block_public_acls_s3/src.rego: -------------------------------------------------------------------------------- 1 | # @title Block Public Access of S3 Buckets 2 | # 3 | # S3 Block Public Access ensures that objects in a bucket never have public access, now and in the future. 4 | # S3 Block Public Access settings override S3 permissions that allow public access. 5 | # If an object is written to an S3 bucket with S3 Block Public Access enabled, and that object specifies any type of public permissions 6 | # via ACL or policy, those public permissions are blocked. 7 | # 8 | # Unintentionally exposed S3 Buckets are a frequent source of data breaches and restricting public access helps prevent unintended data exposure. 9 | # 10 | # See . 11 | package terraform_block_public_acls_s3 12 | 13 | import data.util_functions 14 | 15 | policyID := "AWSSEC-0004" 16 | 17 | # Make sure each S3 Bucket defined has a Public Access block defined for it. 18 | violation[{"policyId": policyID, "msg": msg}] { 19 | input.resource.aws_s3_bucket[bucket_name] 20 | 21 | check_is_bucket_missing_public_access_block(bucket_name) 22 | msg := sprintf("Public access is not explicitly disabled on the following S3 Bucket: `%s`. You must set an `[s3_bucket_public_access_block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block)`.", [bucket_name]) 23 | } 24 | 25 | # Check to see if the public acl flag is not set. 26 | violation[{"policyId": policyID, "msg": msg}] { 27 | input.resource.aws_s3_bucket[bucket_name] 28 | acl_details := get_public_access_block_for_bucket(bucket_name) 29 | 30 | check_if_block_public_acls_is_missing(acl_details) 31 | msg := sprintf("Missing s3 block public access for the following resource(s): `%s`. Required flag: `%s`", [bucket_name, "block_public_acls"]) 32 | } 33 | 34 | # Checks to see if public acls are not blocked. 35 | violation[{"policyId": policyID, "msg": msg}] { 36 | input.resource.aws_s3_bucket[bucket_name] 37 | acl_details := bucket_to_publicAccess(bucket_name) 38 | 39 | check_public_acls_not_blocked(acl_details) 40 | msg := sprintf("Missing s3 block public access for the following resource(s): `%s`. Required flag: `%s`", [bucket_name, "block_public_acls"]) 41 | } 42 | 43 | bucket_to_publicAccess(bucket_name) = public_access_block { 44 | input.resource.aws_s3_bucket[bucket_name] 45 | public_access_block := get_public_access_block_for_bucket(bucket_name) 46 | } 47 | 48 | check_is_bucket_missing_public_access_block(bucket_name) { 49 | not get_public_access_block_for_bucket(bucket_name) 50 | } 51 | 52 | check_if_block_public_acls_is_missing(acl_details) { 53 | not util_functions.has_key(acl_details, "block_public_acls") 54 | } 55 | 56 | check_public_acls_not_blocked(acl_details) { 57 | acl_details.block_public_acls == false 58 | } 59 | 60 | bucket_names_match(first_bucket_name, second_bucket_name) { 61 | first_bucket_name == second_bucket_name 62 | } 63 | 64 | get_public_access_block_for_bucket(bucket_name) = acl_details { 65 | acl_details := input.resource.aws_s3_bucket_public_access_block[_] 66 | 67 | # Since terraform source code is scanned (not `terraform plan`), it is reasonable to expect 68 | # the s3 bucket resource to exist in the same file as the s3 public access block resource. 69 | # "${aws_s3_bucket.example.id}" -> ["${aws_s3_bucket", "example", "id}"] 70 | s3_bucket_id := split(acl_details.bucket, ".") 71 | 72 | bucket_names_match(bucket_name, s3_bucket_id[1]) 73 | } 74 | -------------------------------------------------------------------------------- /policies/packages/nodejs_use_publishConfig/src_test.rego: -------------------------------------------------------------------------------- 1 | package nodejs_package_must_use_org_publish_config 2 | 3 | mockConftestData := { 4 | "MOCKED": true, 5 | "file": { 6 | "dir": "/Users/testuser/Documents/conftest-policy-packs", 7 | "name": "package.json", 8 | }, 9 | } 10 | 11 | test_publish_config { 12 | count(violation) == 0 with input as { 13 | "author": "test author", 14 | "bugs": {"url": "https://github.com/rallyhealth/conftest-policy-packs/issues"}, 15 | "description": "Test scope", 16 | "homepage": "https://github.com/rallyhealth/conftest-policy-packs#readme", 17 | "license": "ISC", 18 | "main": "index.js", 19 | "name": "@rally/myapp", 20 | "repository": { 21 | "type": "git", 22 | "url": "git+gith@github.com:rallyhealth/conftest-policy-packs.git", 23 | }, 24 | "publishConfig": {"registry": "https://registry.npmjs.org"}, 25 | "scripts": {"test": "jest"}, 26 | "version": "1.0.0", 27 | } 28 | with data.conftest as mockConftestData 29 | } 30 | 31 | test_missing_publish_config { 32 | count(violation) == 1 with input as { 33 | "author": "test author", 34 | "bugs": {"url": "https://github.com/rallyhealth/conftest-policy-packs/issues"}, 35 | "description": "Test scope", 36 | "homepage": "https://github.com/rallyhealth/conftest-policy-packs#readme", 37 | "license": "ISC", 38 | "main": "index.js", 39 | "name": "@rally/myapp", 40 | "repository": { 41 | "type": "git", 42 | "url": "git+gith@github.com:rallyhealth/conftest-policy-packs.git", 43 | }, 44 | "scripts": {"test": "jest"}, 45 | "version": "1.0.0", 46 | } 47 | with data.conftest as mockConftestData 48 | } 49 | 50 | test_missing_registry { 51 | count(violation) == 1 with input as { 52 | "author": "test author", 53 | "bugs": {"url": "https://github.com/rallyhealth/conftest-policy-packs/issues"}, 54 | "description": "Test scope", 55 | "homepage": "https://github.com/rallyhealth/conftest-policy-packs#readme", 56 | "license": "ISC", 57 | "main": "index.js", 58 | "name": "@rally/myapp", 59 | "repository": { 60 | "type": "git", 61 | "url": "git+gith@github.com:rallyhealth/conftest-policy-packs.git", 62 | }, 63 | "publishConfig": {}, 64 | "scripts": {"test": "jest"}, 65 | "version": "1.0.0", 66 | } 67 | with data.conftest as mockConftestData 68 | } 69 | 70 | test_publish_config_bad_url { 71 | count(violation) == 1 with input as { 72 | "author": "test author", 73 | "bugs": {"url": "https://github.com/rallyhealth/conftest-policy-packs/issues"}, 74 | "description": "Test scope", 75 | "homepage": "https://github.com/rallyhealth/conftest-policy-packs#readme", 76 | "license": "ISC", 77 | "main": "index.js", 78 | "name": "@rally/myapp", 79 | "repository": { 80 | "type": "git", 81 | "url": "git+gith@github.com:rallyhealth/conftest-policy-packs.git", 82 | }, 83 | "publishConfig": {"registry": "https://someother.domain.com"}, 84 | "scripts": {"test": "jest"}, 85 | "version": "1.0.0", 86 | } 87 | with data.conftest as mockConftestData 88 | } 89 | 90 | test_publish_config_no_url { 91 | count(violation) == 1 with input as { 92 | "author": "test author", 93 | "bugs": {"url": "https://github.com/rallyhealth/conftest-policy-packs/issues"}, 94 | "description": "Test scope", 95 | "homepage": "https://github.com/rallyhealth/conftest-policy-packs#readme", 96 | "license": "ISC", 97 | "main": "index.js", 98 | "name": "@rally/myapp", 99 | "repository": { 100 | "type": "git", 101 | "url": "git+gith@github.com:rallyhealth/conftest-policy-packs.git", 102 | }, 103 | "publishConfig": {"registry": ""}, 104 | "scripts": {"test": "jest"}, 105 | "version": "1.0.0", 106 | } 107 | with data.conftest as mockConftestData 108 | } 109 | -------------------------------------------------------------------------------- /policies/terraform/block_public_acls_s3/src_test.rego: -------------------------------------------------------------------------------- 1 | package terraform_block_public_acls_s3 2 | 3 | test_has_block_public_acl { 4 | count(violation) == 0 with input as {"resource": { 5 | "aws_s3_bucket": {"example": {"bucket": "example"}}, 6 | "aws_s3_bucket_public_access_block": {"example": { 7 | "block_public_acls": true, 8 | "bucket": "${aws_s3_bucket.example.id}", 9 | }}, 10 | }} 11 | } 12 | 13 | test_missing_block_public_acl { 14 | count(violation) == 1 with input as {"resource": { 15 | "aws_s3_bucket": {"example1": {"bucket": "example1"}}, 16 | "aws_s3_bucket_public_access_block": {"example": {"bucket": "${aws_s3_bucket.example1.id}"}}, 17 | }} 18 | } 19 | 20 | test_has_block_public_acl_set_to_false { 21 | count(violation) == 1 with input as {"resource": { 22 | "aws_s3_bucket": {"example2": {"bucket": "example2"}}, 23 | "aws_s3_bucket_public_access_block": {"example": { 24 | "block_public_acls": false, 25 | "bucket": "${aws_s3_bucket.example2.id}", 26 | }}, 27 | }} 28 | } 29 | 30 | test_has_block_public_acl_set_to_true_in_one_but_is_incorrect_in_second_bucket { 31 | count(violation) == 1 with input as {"resource": { 32 | "aws_s3_bucket": { 33 | "example": {"bucket": "example"}, 34 | "other_bucket": {"bucket": "other_example"}, 35 | }, 36 | "aws_s3_bucket_public_access_block": { 37 | "example": { 38 | "block_public_acls": true, 39 | "bucket": "${aws_s3_bucket.example.id}", 40 | }, 41 | "other_block": { 42 | "block_public_acls": false, 43 | "bucket": "${aws_s3_bucket.other_bucket.id}", 44 | }, 45 | }, 46 | }} 47 | } 48 | 49 | test_has_block_public_acl_set_to_false_in_one_but_is_correct_in_second_bucket { 50 | count(violation) == 1 with input as {"resource": { 51 | "aws_s3_bucket": { 52 | "example": {"bucket": "example"}, 53 | "other_bucket": {"bucket": "other_example"}, 54 | }, 55 | "aws_s3_bucket_public_access_block": { 56 | "example": { 57 | "block_public_acls": false, 58 | "bucket": "${aws_s3_bucket.example.id}", 59 | }, 60 | "other_block": { 61 | "block_public_acls": true, 62 | "bucket": "${aws_s3_bucket.other_bucket.id}", 63 | }, 64 | }, 65 | }} 66 | } 67 | 68 | test_missing_block_public_acl_block_object { 69 | count(violation) == 1 with input as {"resource": {"aws_s3_bucket": {"example": {"bucket": "example"}}}} 70 | } 71 | 72 | test_not_s3_bucket { 73 | count(violation) == 0 with input as {"resource": {"aws_instance": {"fake-server": {}}}} 74 | } 75 | 76 | test_missing_block_public_acl_for_defined_bucket { 77 | count(violation) == 1 with input as {"resource": { 78 | "aws_s3_bucket": {"example": {"bucket": "example"}}, 79 | "aws_s3_bucket_public_access_block": {"example": { 80 | "block_public_acls": true, 81 | "bucket": "${aws_s3_bucket.example3.id}", 82 | }}, 83 | }} 84 | } 85 | 86 | test_multiple_buckets_each_have_valid_acls { 87 | count(violation) == 0 with input as {"resource": { 88 | "aws_s3_bucket": { 89 | "example": {"bucket": "example"}, 90 | "other_bucket": {"bucket": "other_example"}, 91 | }, 92 | "aws_s3_bucket_public_access_block": { 93 | "example": { 94 | "block_public_acls": true, 95 | "bucket": "${aws_s3_bucket.example.id}", 96 | }, 97 | "other_block": { 98 | "block_public_acls": true, 99 | "bucket": "${aws_s3_bucket.other_bucket.id}", 100 | }, 101 | }, 102 | }} 103 | } 104 | 105 | test_multiple_buckets_each_have_invalid_acls { 106 | count(violation) == 2 with input as {"resource": { 107 | "aws_s3_bucket": { 108 | "example": {"bucket": "example"}, 109 | "other_bucket": {"bucket": "other_example"}, 110 | }, 111 | "aws_s3_bucket_public_access_block": { 112 | "example": { 113 | "block_public_acls": false, 114 | "bucket": "${aws_s3_bucket.example.id}", 115 | }, 116 | "other_block": { 117 | "block_public_acls": false, 118 | "bucket": "${aws_s3_bucket.other_bucket.id}", 119 | }, 120 | }, 121 | }} 122 | } 123 | -------------------------------------------------------------------------------- /policies/docker/deny_image_unless_from_registry/src_test.rego: -------------------------------------------------------------------------------- 1 | package docker_pull_from_registry 2 | 3 | test_pull_registry { 4 | count(violation) == 0 with input as [ 5 | {"Cmd": "from", "Flags": [], "JSON": false, "SubCmd": "", "Value": ["my.private.registry/ubuntu:20.04"]}, 6 | {"Cmd": "label", "Flags": [], "JSON": false, "SubCmd": "", "Value": ["maintainer", "\"Ari\""]}, 7 | {"Cmd": "run", "Flags": [], "JSON": false, "SubCmd": "", "Value": ["apt update"]}, {"Cmd": "cmd", "Flags": [], "JSON": true, "SubCmd": "", "Value": ["sh"]}, 8 | ] 9 | } 10 | 11 | test_pull_registry_in_second_from { 12 | count(violation) == 1 with input as [ 13 | {"Cmd": "from", "Flags": [], "JSON": false, "SubCmd": "", "Value": ["my.private.registry/ubuntu:20.04"]}, 14 | {"Cmd": "label", "Flags": [], "JSON": false, "SubCmd": "", "Value": ["maintainer", "\"Ari\""]}, 15 | {"Cmd": "from", "Flags": [], "JSON": false, "SubCmd": "", "Value": ["ubuntu:20.04"]}, 16 | {"Cmd": "run", "Flags": [], "JSON": false, "SubCmd": "", "Value": ["apt update"]}, {"Cmd": "cmd", "Flags": [], "JSON": true, "SubCmd": "", "Value": ["sh"]}, 17 | ] 18 | } 19 | 20 | test_pull_dockerhub { 21 | count(violation) == 1 with input as [ 22 | {"Cmd": "from", "Flags": [], "JSON": false, "SubCmd": "", "Value": ["ubuntu:20.04"]}, 23 | {"Cmd": "label", "Flags": [], "JSON": false, "SubCmd": "", "Value": ["maintainer", "\"Ari\""]}, 24 | {"Cmd": "run", "Flags": [], "JSON": false, "SubCmd": "", "Value": ["apt update"]}, {"Cmd": "cmd", "Flags": [], "JSON": true, "SubCmd": "", "Value": ["sh"]}, 25 | ] 26 | } 27 | 28 | test_pull_github_container_registry { 29 | count(violation) == 1 with input as [ 30 | {"Cmd": "from", "Flags": [], "JSON": false, "SubCmd": "", "Value": ["ghcr.io/ubuntu:20.04"]}, 31 | {"Cmd": "label", "Flags": [], "JSON": false, "SubCmd": "", "Value": ["maintainer", "\"Ari\""]}, 32 | {"Cmd": "run", "Flags": [], "JSON": false, "SubCmd": "", "Value": ["apt update"]}, {"Cmd": "cmd", "Flags": [], "JSON": true, "SubCmd": "", "Value": ["sh"]}, 33 | ] 34 | } 35 | 36 | test_dockerfile_with_from_variables { 37 | count(violation) == 0 with input as [ 38 | {"Cmd": "arg", "Flags": [], "JSON": false, "SubCmd": "", "Value": ["AIRFLOW_VERSION=\"1.10.12\""]}, 39 | {"Cmd": "arg", "Flags": [], "JSON": false, "SubCmd": "", "Value": ["AIRFLOW_PYTHON_VERSION=\"3.6\""]}, 40 | {"Cmd": "arg", "Flags": [], "JSON": false, "SubCmd": "", "Value": ["AIRFLOW_IMAGE=\"my.private.registry/apache/airflow\""]}, 41 | {"Cmd": "from", "Flags": [], "JSON": false, "SubCmd": "", "Value": ["$AIRFLOW_IMAGE:$AIRFLOW_PYTHON_VERSION", "as", "imageWithVariables"]}, 42 | {"Cmd": "cmd", "Flags": [], "JSON": true, "SubCmd": "", "Value": ["echo"]}, 43 | ] 44 | } 45 | 46 | test_dockerfile_with_image_in_arg { 47 | count(violation) == 1 with input as [ 48 | {"Cmd": "arg", "Flags": [], "JSON": false, "SubCmd": "", "Value": ["AIRFLOW_VERSION=\"1.10.12\""]}, 49 | {"Cmd": "arg", "Flags": [], "JSON": false, "SubCmd": "", "Value": ["AIRFLOW_PYTHON_VERSION=\"3.6\""]}, 50 | {"Cmd": "arg", "Flags": [], "JSON": false, "SubCmd": "", "Value": ["AIRFLOW_IMAGE=\"apache/airflow\""]}, 51 | {"Cmd": "from", "Flags": [], "JSON": false, "SubCmd": "", "Value": ["$AIRFLOW_IMAGE:$AIRFLOW_PYTHON_VERSION", "as", "imageWithVariables"]}, 52 | {"Cmd": "from", "Flags": [], "JSON": false, "SubCmd": "", "Value": ["imageWithVariables", "AS", "multiStage"]}, 53 | {"Cmd": "cmd", "Flags": [], "JSON": true, "SubCmd": "", "Value": ["echo"]}, 54 | ] 55 | } 56 | 57 | test_dockerfile_with_image_in_env { 58 | count(violation) == 1 with input as [ 59 | {"Cmd": "env", "Flags": [], "JSON": false, "SubCmd": "", "Value": ["AIRFLOW_VERSION=\"1.10.12\""]}, 60 | {"Cmd": "env", "Flags": [], "JSON": false, "SubCmd": "", "Value": ["AIRFLOW_PYTHON_VERSION=\"3.6\""]}, 61 | {"Cmd": "env", "Flags": [], "JSON": false, "SubCmd": "", "Value": ["AIRFLOW_IMAGE=\"apache/airflow\""]}, 62 | {"Cmd": "from", "Flags": [], "JSON": false, "SubCmd": "", "Value": ["$AIRFLOW_IMAGE:$AIRFLOW_PYTHON_VERSION", "as", "imageWithVariables"]}, 63 | {"Cmd": "from", "Flags": [], "JSON": false, "SubCmd": "", "Value": ["imageWithVariables", "AS", "multiStage"]}, 64 | {"Cmd": "cmd", "Flags": [], "JSON": true, "SubCmd": "", "Value": ["echo"]}, 65 | ] 66 | } 67 | 68 | test_dockerfile_with_multistage_build { 69 | count(violation) == 0 with input as [ 70 | {"Cmd": "from", "Flags": [], "JSON": false, "SubCmd": "", "Value": ["my.private.registry/ubuntu:20.04", "AS", "builder"]}, 71 | {"Cmd": "from", "Flags": [], "JSON": false, "SubCmd": "", "Value": ["builder", "AS", "multiStage"]}, 72 | {"Cmd": "cmd", "Flags": [], "JSON": true, "SubCmd": "", "Value": ["echo"]}, 73 | ] 74 | } 75 | 76 | test_from_scratch { 77 | count(violation) == 0 with input as [ 78 | {"Cmd": "from", "Flags": [], "JSON": false, "SubCmd": "", "Value": ["scratch"]}, 79 | {"Cmd": "entrypoint", "Flags": [], "JSON": false, "SubCmd": "", "Value": ["/app/cmd"]}, 80 | {"Cmd": "cmd", "Flags": [], "JSON": true, "SubCmd": "", "Value": ["--help"]}, 81 | ] 82 | } 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Conftest Policy Packs 2 | 3 | [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/conftest-policy-packs)](https://artifacthub.io/packages/search?repo=conftest-policy-packs) 4 | [![Conftest CI](https://github.com/rallyhealth/conftest-policy-packs/actions/workflows/ci.yml/badge.svg)](https://github.com/rallyhealth/conftest-policy-packs/actions/workflows/ci.yml) 5 | ![Github Contributors](https://img.shields.io/github/contributors/rallyhealth/conftest-policy-packs) 6 | ![GitHub last commit](https://img.shields.io/github/last-commit/rallyhealth/conftest-policy-packs) 7 | ![GitHub](https://img.shields.io/github/license/rallyhealth/conftest-policy-packs) 8 | 9 | Centralized OPA policy workflow for Conftest-based Compliance-as-Code evaluations. 10 | 11 | This is a central repository housing a snapshot of Rally Health's Rego policies for its Compliance-as-Code program. 12 | 13 | View this project at it's [GitHub Page](https://rallyhealth.github.io/conftest-policy-packs/). 14 | The policy documentation is available [here](https://rallyhealth.github.io/conftest-policy-packs/docs/policies.html). 15 | 16 | Rally enforces these policies through a homegrown GitHub App running on AWS ECS to evaluate every commit in the organization. 17 | The GitHub App is an internal wrapper around Conftest, primarily handling reporting the violation results back to developer workflows 18 | in GitHub pull requests and to a central dashboard in Datadog. 19 | Violations are reported as non-blocking status checks. 20 | Results are added to developer PRs within 4-10 seconds. 21 | This repository only houses the policies used in this process and demonstrates a CI/CD approach to creating and managing 22 | Rego policies. 23 | 24 | Policy messages in each Rego file use markdown syntax as Rally publishes policy messages to pull request status checks. 25 | 26 | ![policy violation markdown](/docs/images/violation-markdown-example.png) 27 | 28 | _(Note the details in that image, such as the policy ID and message, may not line up with the policies currently in this repo.)_ 29 | 30 | 31 | - [Usage](#usage) 32 | - [Downloading Policies](#downloading-policies) 33 | - [Policy Data](#policy-data) 34 | - [Policy Exceptions](#policy-exceptions) 35 | - [Contributing](#contributing) 36 | - [Contributing A Policy](#contributing-a-policy) 37 | - [Quick Start](#quick-start) 38 | - [Run tests](#run-tests) 39 | - [Convert file into JSON for unit tests](#convert-file-into-json-for-unit-tests) 40 | - [Debug a policy](#debug-a-policy) 41 | - [Generate documentation](#generate-documentation) 42 | - [Original Contributors](#original-contributors) 43 | 44 | 45 | # Usage 46 | 47 | Policy documentation can be found [here](/docs/policies.md). 48 | 49 | ## Downloading Policies 50 | 51 | Follow Conftest's [instructions for sharing policies](https://www.conftest.dev/sharing/). 52 | 53 | You can pull policies directly from this repo: 54 | 55 | ```bash 56 | # Git SSH syntax 57 | conftest pull git::git@github.com:rallyhealth/conftest-policy-packs.git//policies 58 | # Git HTTPS syntax 59 | conftest pull git::https://github.com/rallyhealth/conftest-policy-packs.git//policies 60 | ``` 61 | 62 | ![Conftest pull](/docs/images/pulling-policies.png) 63 | 64 | See `conftest pull --help` for more instructions on customizing the download, if needed. 65 | 66 | These policies will soon be available on [CNCF Artifact Hub](https://artifacthub.io/). 67 | 68 | ## Policy Data 69 | 70 | These policies are provided for general consumption. 71 | Policy contents are written to be general purpose and org-specific values are relegated to the `data/` directory for import via conftest `--data`. 72 | You should pull the policies with `conftest pull` and specify your own data files as appropriate for your organization. 73 | Explanations for each value are provided in the YAML files under `data/`. 74 | 75 | ## Policy Exceptions 76 | 77 | Rally currently handles exceptions in its homegrown GitHub App code wrapping Conftest. 78 | Violations produced by Conftest are filtered out from a JSON mapping of approved exceptions to repos. 79 | Exceptions are supported at a per-file level. 80 | 81 | See [here](/exceptions/readme.md) for more information. 82 | 83 | # Contributing 84 | 85 | Please follow the [contribution instructions](/CONTRIBUTING.md). 86 | 87 | Generally: 88 | 89 | 1. Fork the repository 90 | 1. Create your feature branch 91 | 1. Commit your changes following [semantic commit syntax](/CONTRIBUTING.md#commits) 92 | 1. Push to the branch 93 | 1. Open a pull request 94 | 95 | ## Contributing A Policy 96 | 97 | Follow the requisite section in the [contribution instructions](/CONTRIBUTING.md#adding-rego-policies). 98 | 99 | ## Quick Start 100 | 101 | ### Run tests 102 | 103 | ```bash 104 | make test 105 | ``` 106 | 107 | ### Convert file into JSON for unit tests 108 | 109 | ```bash 110 | conftest parse 111 | ``` 112 | 113 | ### Debug a policy 114 | 115 | 1. Use `trace()` in the policy 116 | 1. Run conftest with `--trace` 117 | 1. Recommended, pipe the output to grep (`| grep Note`) to only view the `trace()` output 118 | 119 | Also use the [OPA playground](https://play.openpolicyagent.org/) to troubleshoot Rego code. 120 | 121 | ### Generate documentation 122 | 123 | ```bash 124 | make docs 125 | ``` 126 | 127 | # Original Contributors 128 | 129 | Rally's compliance-as-code program has seen early success internally thanks to the following individuals who contributed to the effort: 130 | 131 | - Ari Kalfus 132 | - Mia Kralowetz 133 | - Nicholas Hung 134 | - Karl Nilsen 135 | - Benjamin Mangold 136 | -------------------------------------------------------------------------------- /policies/packages/nodejs_must_use_recent_version/src.rego: -------------------------------------------------------------------------------- 1 | # @title NodeJS Projects Must Use A Recent NodeJS Version 2 | # 3 | # NodeJS projects must use a recent NodeJS release. 4 | # Only 1 LTS version is active at one time, however we allow 1 previous LTS version to be used to 5 | # accomodate upgrade migration periods. 6 | # 7 | # You may use a non-LTS "current" NodeJS release as long as that version is more current than the most recently 8 | # deprecated LTS release. 9 | # For example, if the LTS version is 16 (meaning version 14 was the most recently deprecated LTS version), 10 | # you may use NodeJS 14, 15, 16, or 17. 11 | # Once NodeJS 18 is released as an LTS version, you may use versions 16, 17, 18, or 19. 12 | # 13 | # See for more information about Node's release schedule. 14 | # 15 | # See for more information about the `engines` 16 | # field in `package.json` files. 17 | # 18 | # | :memo: This policy is "online" in that it makes an HTTP request to raw.githubusercontent.com and requires connectivity to receive a response. | 19 | # | --- | 20 | package nodejs_must_use_approved_version 21 | 22 | import data.packages_functions 23 | import data.util_functions 24 | 25 | policyID := "PKGSEC-0002" 26 | 27 | nodejs_release_schedule_json := "https://raw.githubusercontent.com/nodejs/Release/main/schedule.json" 28 | 29 | get_nodejs_releases = output { 30 | # This is a separate function (with no input params) to allow us to mock the call in the tests 31 | output := http.send({ 32 | "url": nodejs_release_schedule_json, 33 | "method": "GET", 34 | "force_json_decode": true, 35 | "cache": true, 36 | }) 37 | } 38 | 39 | get_latest_lts_version = latest_lts_release { 40 | output := get_nodejs_releases 41 | 42 | releases := filter_lts_releases(output) 43 | num_releases := count(releases) 44 | 45 | # This may be an LTS in the future, not the currently released "latest" LTS version 46 | # This would be an even-numbered current release with a start date in the future, when it becomes the LTS release 47 | latest_lts := releases[minus(num_releases, 1)] 48 | 49 | # e.g. { "codename": "", "end": "2025-04-30", "lts": "2022-10-25", "maintenance": "2023-10-18", "start": "2022-04-19" } 50 | release_metadata := output.body[sprintf("v%d", [latest_lts])] 51 | 52 | # Output is a float representing the difference in nanoseconds between the two dates 53 | # All we care about is whether it is positive or negative 54 | time_diff := determine_time_difference_between_today_and_latest_lts(release_metadata) 55 | 56 | # This will either return the latest LTS release or the second-latest, depending on that time difference outcome 57 | latest_lts_release := determine_current_lts_release(releases, time_diff) 58 | } 59 | 60 | filter_lts_releases(output) = sorted_releases { 61 | # We want the latest LTS release 62 | # This comprehension will filter out versions that do not contain an 'lts' field 63 | # Leaving us with only the LTS releases 64 | # We prune the 'v' in the versions and convert them to numbers 65 | # e.g. "v16" -> 16 66 | # and sort so the highest number (latest release) is at the end of the list 67 | releases := [to_number(substring(version, 1, -1)) | record := output.body[version]; record.lts] 68 | sorted_releases := sort(releases) 69 | } 70 | 71 | determine_time_difference_between_today_and_latest_lts(release_metadata) = time_diff { 72 | today := time.now_ns() 73 | 74 | # Layout comes from requirements in Golang time.Parse 75 | # https://golang.org/pkg/time/#Parse 76 | release_time := time.parse_ns("2006-01-02", release_metadata.start) 77 | time_diff := today - release_time 78 | } 79 | 80 | determine_current_lts_release(sorted_releases, time_diff) = sorted_releases[minus(count(sorted_releases), 1)] { 81 | # If release time is in the future, use the second-latest LTS, which would be the current LTS version 82 | # If time diff is positive, then LTS release comes out in the future 83 | # If time diff is negative, then the LTS release came out before this moment 84 | time_diff >= 0 85 | } else = sorted_releases[minus(count(sorted_releases), 2)] { 86 | true 87 | } 88 | 89 | has_node_engine(resource) { 90 | util_functions.has_key(resource, "engines") 91 | util_functions.has_key(resource.engines, "node") 92 | } 93 | 94 | is_unapproved_node_version(engine_string) { 95 | # This is going to strip symbols from the string then try to convert it into a number. 96 | # It is possible to have multiple version constraints in the node engine string. 97 | # This function attempts to require every version in the string to comply with the LTS policy. 98 | # e.g. ">=10 <15" => ["10", "15"] (possible_multiple_versions variable) 99 | # It will trigger a violation if any version number in the string is outside the acceptable range. 100 | 101 | # List any possible symbols or other characters we don't care about that are valid in the engine string 102 | engine_string_no_symbols := strings.replace_n({ 103 | "<": "", 104 | ">": "", 105 | "=": "", 106 | "~": "", 107 | }, engine_string) 108 | 109 | possible_multiple_versions := split(engine_string_no_symbols, " ") 110 | numbers_outside_acceptable_range(possible_multiple_versions) 111 | } 112 | 113 | numbers_outside_acceptable_range(number_string_list) { 114 | some i 115 | version := to_number(number_string_list[i]) 116 | version < latest_lts_version - 2 117 | } 118 | 119 | missing_minimum_version_constraint(engine_string) { 120 | index_of_minimum_constraint := indexof(engine_string, ">") 121 | index_of_minimum_constraint == -1 122 | } 123 | 124 | latest_lts_version := get_latest_lts_version 125 | 126 | violation[{"policyId": policyID, "msg": msg}] { 127 | packages_functions.is_package_json(input) 128 | not has_node_engine(input) 129 | msg := sprintf("NodeJS projects must enforce a Node engine version within the last 2 LTS releases. This project does not enforce any Node engine version. See the [NodeJS documentation](https://docs.npmjs.com/cli/v7/configuring-npm/package-json#engines) on how to require a Node version. You must use a version of Node >= %d.", [latest_lts_version - 2]) 130 | } 131 | 132 | violation[{"policyId": policyID, "msg": msg}] { 133 | packages_functions.is_package_json(input) 134 | has_node_engine(input) 135 | is_unapproved_node_version(input.engines.node) 136 | msg := sprintf("NodeJS projects must enforce a Node engine version within the last 2 LTS releases. This project uses an older NodeJS version in its engine constraint: [`%s`]. You must use a version of Node >= %d.", [input.engines.node, latest_lts_version - 2]) 137 | } 138 | 139 | violation[{"policyId": policyID, "msg": msg}] { 140 | packages_functions.is_package_json(input) 141 | has_node_engine(input) 142 | missing_minimum_version_constraint(input.engines.node) 143 | msg := sprintf("NodeJS projects must enforce a Node engine version within the last 2 LTS releases. This project does not enforce a minimum Node engine version. You must use a version of Node >= %d.", [latest_lts_version - 2]) 144 | } 145 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 4 | - [Local Setup](#local-setup) 5 | - [IDE Plugin](#ide-plugin) 6 | - [Commits](#commits) 7 | - [Adding Rego Policies](#adding-rego-policies) 8 | - [Writing Policies](#writing-policies) 9 | - [Policy Documentation](#policy-documentation) 10 | - [Policy ID](#policy-id) 11 | - [Testing Policies](#testing-policies) 12 | - [Troubleshooting Policies](#troubleshooting-policies) 13 | 14 | 15 | # Local Setup 16 | 17 | [Install Homebrew](https://brew.sh/) on OSX or Linux. 18 | 19 | Run `make install`. 20 | 21 | We use [konstraint](https://github.com/plexsystems/konstraint) to generate Rego policy documentation and [mdtoc](https://github.com/kubernetes-sigs/mdtoc) to 22 | generate markdown table-of-contents. 23 | 24 | ## IDE Plugin 25 | 26 | We recommend the [official Open Policy Agent](https://plugins.jetbrains.com/plugin/14865-open-policy-agent) plugin 27 | for Jetbrains IDEs. 28 | 29 | We recommend [tsandall.opa](https://marketplace.visualstudio.com/items?itemName=tsandall.opa) for Visual Studio Code. 30 | 31 | ## Commits 32 | 33 | This project follows [semantic commit messages](https://karma-runner.github.io/latest/dev/git-commit-msg.html). 34 | 35 | Format of a commit message: 36 | 37 | ``` 38 | (): 39 | 40 | 41 | 42 |