Skip to content

Commit

Permalink
feat: Use Regal for Rego linting (#881)
Browse files Browse the repository at this point in the history
This PR introduces [Regal](https://github.com/styrainc/regal) for linting
the example policies in this project. The Rego found there is generally in
a good shape, and the fixes done as part of this are mostly related to style.

The code has been updated to conformance against the following rules:

* [line-length](https://docs.styra.com/regal/rules/style/line-length)
* [use-assignment-operator](https://docs.styra.com/regal/rules/style/use-assignment-operator)
* [use-some-for-output-vars](https://docs.styra.com/regal/rules/idiomatic/use-some-for-output-vars)
* [prefer-snake-case](https://docs.styra.com/regal/rules/style/prefer-snake-case)
* [print-or-trace-call](https://docs.styra.com/regal/rules/testing/print-or-trace-call)

A few rules are also ignored for now (`.regal/config.yaml`) and I'll leave
it up to the maintainers to decide whether more rules should be enabled
later.

The PR also adds Regal to CI to lint any Rego introduced or changed
in future PRs.

For more information about Regal and its rules, see the
[documentation](https://docs.styra.com/regal) for the project.

Signed-off-by: Anders Eknert <anders@styra.com>
  • Loading branch information
anderseknert committed Oct 19, 2023
1 parent e082ab6 commit 14e95ad
Show file tree
Hide file tree
Showing 28 changed files with 107 additions and 44 deletions.
8 changes: 8 additions & 0 deletions .github/workflows/pr.yaml
Expand Up @@ -56,6 +56,14 @@ jobs:
- name: test examples
run: make test-examples

- name: setup regal
uses: StyraInc/setup-regal@v0.2.0
with:
version: v0.11.0

- name: lint examples
run: regal lint --format github examples

- name: acceptance
run: make test-acceptance

Expand Down
19 changes: 19 additions & 0 deletions .regal/config.yaml
@@ -0,0 +1,19 @@
rules:
idiomatic:
no-defined-entrypoint:
level: ignore
imports:
prefer-package-imports:
level: error
ignore-import-paths:
- data.services
style:
line-length:
non-breakable-word-threshold: 80
opa-fmt:
level: ignore
prefer-some-in-iteration:
level: ignore
testing:
test-outside-test-package:
level: ignore
12 changes: 6 additions & 6 deletions acceptance.bats
Expand Up @@ -176,7 +176,7 @@
@test "Supports print() output" {
run ./conftest test -p examples/report/policy_print/labels.rego examples/kubernetes/deployment.yaml --no-color
[ "$status" -eq 1 ]
[[ "${lines[0]}" == "PRNT examples/report/policy_print/labels.rego:12: hello-kubernetes" ]]
[[ "${lines[0]}" == "PRNT examples/report/policy_print/labels.rego:13: hello-kubernetes" ]]
}

@test "Can parse hcl1 files" {
Expand Down Expand Up @@ -282,7 +282,7 @@
}

@test "Can parse newly introduced keywords for docker" {
run bash -c "cat <<EOF | ./conftest parse --parser dockerfile -
run bash -c "cat <<EOF | ./conftest parse --parser dockerfile -
# syntax=docker/dockerfile:1.4
FROM alpine
COPY --link /foo /bar
Expand Down Expand Up @@ -415,14 +415,14 @@ EOF"
}

@test "Can combine yaml files" {
run ./conftest test -p examples/combine/policy examples/combine/team.yaml examples/combine/user1.yaml examples/combine/user2.yaml --combine
run ./conftest test -p examples/combine/policy examples/combine/team.yaml examples/combine/user1.yaml examples/combine/user2.yaml --combine

[ "$status" -eq 1 ]
[[ "$output" =~ "2 tests, 1 passed, 0 warnings, 1 failure" ]]
}

@test "Combining multi-document yaml file has same result" {
run ./conftest test -p examples/combine/policy examples/combine/team.yaml examples/combine/users.yaml --combine
run ./conftest test -p examples/combine/policy examples/combine/team.yaml examples/combine/users.yaml --combine

[ "$status" -eq 1 ]
[[ "$output" =~ "2 tests, 1 passed, 0 warnings, 1 failure" ]]
Expand All @@ -438,7 +438,7 @@ EOF"
run ./conftest test -p examples/spdx/policy examples/spdx/sbom.spdx

[ "$status" -eq 0 ]
[[ "$output" =~ "1 test, 1 passed, 0 warnings, 0 failures, 0 exceptions" ]]
[[ "$output" =~ "1 test, 1 passed, 0 warnings, 0 failures, 0 exceptions" ]]
}

@test "Can test cyclonedx against policy" {
Expand Down Expand Up @@ -499,7 +499,7 @@ EOF"
}

@test "Should show output because of failure" {
run ./conftest test -p examples/kubernetes/policy/ examples/kubernetes/deployment.yaml --all-namespaces --quiet
run ./conftest test -p examples/kubernetes/policy/ examples/kubernetes/deployment.yaml --all-namespaces --quiet
[ "$status" -eq 1 ]
[[ "$output" =~ "5 tests, 1 passed, 0 warnings, 4 failures, 0 exceptions" ]]
}
Expand Down
6 changes: 3 additions & 3 deletions examples/awssam/policy/policy.rego
@@ -1,15 +1,15 @@
package main

denylist = ["*"]
denylist := ["*"]

sensitive_denylist = [
sensitive_denylist := [
"password",
"Password",
"Pass",
"pass",
]

runtime_denylist = [
runtime_denylist := [
"python2.7",
"node4.3",
]
Expand Down
11 changes: 8 additions & 3 deletions examples/combine/policy/duplicate.rego
Expand Up @@ -3,7 +3,7 @@ package main
# Check that no name attribute exists twice among all resources
deny[msg] {
name := input[_].contents.metadata.name
occurrences := [name | input[i].contents.metadata.name == name; name := input[i].metadata.name]
occurrences := [name | some i; input[i].contents.metadata.name == name; name := input[i].metadata.name]
count(occurrences) > 1
msg = sprintf("Error duplicate name : %s", [name])
}
Expand All @@ -13,8 +13,10 @@ deny[msg] {
name := input[_].contents.metadata.name
kind == "team"

some i, j

# list all existing users
existing_users = {email | input[i].contents.kind == "user"; email := input[i].contents.metadata.email}
existing_users = {email | some i; input[i].contents.kind == "user"; email := input[i].contents.metadata.email}

# gather all configured users in teams
configured_owner_users_array = [user | input[i].contents.kind == "team"; user := input[i].contents.spec.owner]
Expand All @@ -31,5 +33,8 @@ deny[msg] {
# missing users are the ones configured in teams but not in Github
count(missing_users) > 0

msg = sprintf("Existing users %s | Configured users %s | Missing users %s", [sort(existing_users), sort(configured_users), sort(missing_users)])
msg = sprintf(
"Existing users %s | Configured users %s | Missing users %s",
[sort(existing_users), sort(configured_users), sort(missing_users)],
)
}
8 changes: 5 additions & 3 deletions examples/cyclonedx/policy/policy.rego
@@ -1,7 +1,9 @@
package main

deny[msg] {
expectedSHAS256 := "sha256:d7ec60cf8390612b360c857688b383068b580d9a6ab78417c9493170ad3f1616"
input.metadata.component.version != expectedSHAS256
msg := sprintf("current SHA256 %s is not equal to expected SHA256 %s", [input.metadata.component.version, expectedSHAS256])
expected_shas256 := "sha256:d7ec60cf8390612b360c857688b383068b580d9a6ab78417c9493170ad3f1616"
input.metadata.component.version != expected_shas256
msg := sprintf(
"current SHA256 %s is not equal to expected SHA256 %s", [input.metadata.component.version, expected_shas256]
)
}
5 changes: 3 additions & 2 deletions examples/docker/policy/commands.rego
@@ -1,6 +1,6 @@
package commands

denylist = [
denylist := [
"apk",
"apt",
"pip",
Expand All @@ -9,9 +9,10 @@ denylist = [
]

deny[msg] {
some i
input[i].Cmd == "run"
val := input[i].Value
contains(val[_], denylist[_])

msg = sprintf("unallowed commands found %s", [val])
msg := sprintf("unallowed commands found %s", [val])
}
3 changes: 2 additions & 1 deletion examples/docker/policy/images.rego
@@ -1,8 +1,9 @@
package main

denylist = ["openjdk"]
denylist := ["openjdk"]

deny[msg] {
some i
input[i].Cmd == "from"
val := input[i].Value
contains(val[i], denylist[_])
Expand Down
4 changes: 2 additions & 2 deletions examples/hcl1/policy/base.rego
@@ -1,14 +1,14 @@
package main

denylist = [
denylist := [
"google_iam",
"google_container",
]

deny[msg] {
check_resources(input.resource_changes, denylist)
banned := concat(", ", denylist)
msg = sprintf("Terraform plan will change prohibited resources in the following namespaces: %v", [banned])
msg := sprintf("Terraform plan will change prohibited resources in the following namespaces: %v", [banned])
}

# Checks whether the plan will cause resources with certain prefixes to change
Expand Down
3 changes: 3 additions & 0 deletions examples/hcl2/policy/deny.rego
Expand Up @@ -5,19 +5,22 @@ has_field(obj, field) {
}

deny[msg] {
some lb
proto := input.resource.aws_alb_listener[lb].protocol
proto == "HTTP"
msg = sprintf("ALB `%v` is using HTTP rather than HTTPS", [lb])
}

deny[msg] {
some name
rule := input.resource.aws_security_group_rule[name]
rule.type == "ingress"
contains(rule.cidr_blocks[_], "0.0.0.0/0")
msg = sprintf("ASG `%v` defines a fully open ingress", [name])
}

deny[msg] {
some name
disk = input.resource.azurerm_managed_disk[name]
has_field(disk, "encryption_settings")
disk.encryption_settings.enabled != true
Expand Down
4 changes: 3 additions & 1 deletion examples/hcl2/policy/deny_test.rego
Expand Up @@ -13,7 +13,9 @@ test_blank_input {
}

test_correctly_encrypted_azure_disk {
no_violations with input as {"resource": {"azurerm_managed_disk": {"sample": {"encryption_settings": {"enabled": true}}}}}
no_violations with input as {
"resource": {"azurerm_managed_disk": {"sample": {"encryption_settings": {"enabled": true}}}}
}
}

test_unencrypted_azure_disk {
Expand Down
4 changes: 2 additions & 2 deletions examples/ignore/dockerignore/policy/deny.rego
@@ -1,13 +1,13 @@
package main

any_git_ignored {
entry := input[o]
entry := input[_]

entry.Kind == "Path"
entry.Value == ".git"
}

deny[msg] {
not any_git_ignored
msg = ".git directories should be ignored"
msg := ".git directories should be ignored"
}
2 changes: 1 addition & 1 deletion examples/ignore/gitignore/policy/deny.rego
@@ -1,7 +1,7 @@
package main

any_id_rsa_ignored {
entry := input[i]
entry := input[_]

entry.Kind == "Path"
entry.Value == "id_rsa"
Expand Down
2 changes: 2 additions & 0 deletions examples/kubernetes/combine/combine.rego
@@ -1,13 +1,15 @@
package main

violation[msg] {
some i
input[i].contents.kind == "Deployment"
deployment := input[i].contents
not service_selects_app(deployment.spec.selector.matchLabels.app)
msg := sprintf("Deployment '%v' has no matching service", [deployment.metadata.name])
}

service_selects_app(app) {
some i
input[i].contents.kind == "Service"
service := input[i].contents
service.spec.selector.app == app
Expand Down
12 changes: 10 additions & 2 deletions examples/kubernetes/policy/base_test.rego
Expand Up @@ -13,7 +13,10 @@ no_warnings {
}

test_deployment_without_security_context {
deny["Containers must not run as root in Deployment sample"] with input as {"kind": "Deployment", "metadata": {"name": "sample"}}
deny["Containers must not run as root in Deployment sample"] with input as {
"kind": "Deployment",
"metadata": {"name": "sample"}
}
}

test_deployment_with_security_context {
Expand Down Expand Up @@ -47,5 +50,10 @@ test_services_not_denied {
}

test_services_issue_warning {
warn["Found service sample but services are not allowed"] with input as {"kind": "Service", "metadata": {"name": "sample"}}
warn["Found service sample but services are not allowed"] with input as {
"kind": "Service",
"metadata": {
"name": "sample"
}
}
}
2 changes: 1 addition & 1 deletion examples/kubernetes/policy/deny.rego
Expand Up @@ -2,7 +2,7 @@ package main

import data.kubernetes

name = input.metadata.name
name := input.metadata.name

deny[msg] {
kubernetes.is_deployment
Expand Down
2 changes: 1 addition & 1 deletion examples/kubernetes/policy/labels.rego
Expand Up @@ -2,7 +2,7 @@ package main

import data.kubernetes

name = input.metadata.name
name := input.metadata.name

required_deployment_labels {
input.metadata.labels["app.kubernetes.io/name"]
Expand Down
4 changes: 2 additions & 2 deletions examples/kubernetes/policy/violation.rego
Expand Up @@ -2,9 +2,9 @@ package main

import data.kubernetes

name = input.metadata.name
name := input.metadata.name

violation[{"msg": msg, "details": {}}] {
kubernetes.is_deployment
msg = sprintf("Found deployment %s but deployments are not allowed", [name])
msg := sprintf("Found deployment %s but deployments are not allowed", [name])
}
2 changes: 1 addition & 1 deletion examples/kubernetes/policy/warn.rego
Expand Up @@ -2,7 +2,7 @@ package main

import data.kubernetes

name = input.metadata.name
name := input.metadata.name

warn[msg] {
kubernetes.is_service
Expand Down
4 changes: 3 additions & 1 deletion examples/properties/policy/test.rego
@@ -1,17 +1,19 @@
package main

deny_valid_uri[msg] {
some name
value := input[name]
contains(lower(name), "url")
not contains(lower(value), "http")
msg := sprintf("Must have a valid uri defined '%s'", [value])
}

secret_exceptions = {
secret_exceptions := {
"secret.value.exception"
}

deny_no_secrets[msg] {
some name
value := input[name]
not secret_exceptions[name]
contains(lower(name), "secret")
Expand Down
3 changes: 2 additions & 1 deletion examples/report/policy/labels.rego
@@ -1,6 +1,6 @@
package main

name = input.metadata.name
name := input.metadata.name

required_deployment_labels {
input.metadata.labels["app.kubernetes.io/name"]
Expand All @@ -10,6 +10,7 @@ required_deployment_labels {
deny[msg] {
input.kind = "Deployment"
not required_deployment_labels
# regal ignore:print-or-trace-call
trace("just testing notes flag")
msg := sprintf("%s must include Kubernetes recommended labels: https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/#labels", [name])
}
5 changes: 3 additions & 2 deletions examples/report/policy_print/labels.rego
@@ -1,14 +1,15 @@
package main

name = input.metadata.name
name := input.metadata.name

required_deployment_labels {
input.metadata.labels["app.kubernetes.io/name"]
input.metadata.labels["app.kubernetes.io/instance"]
}

deny[msg] {
input.kind = "Deployment"
input.kind == "Deployment"
# regal ignore:print-or-trace-call
print(name)
not required_deployment_labels
msg := sprintf("%s must include Kubernetes recommended labels: https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/#labels", [name])
Expand Down

0 comments on commit 14e95ad

Please sign in to comment.