Repository: plexsystems/konstraint
Pull request #296 (2022-05-30 18:45:37)

Misc. verbiage fixes
Terminology alignment and typo fixes.

Signed-off-by: James Alseth <james@jalseth.me>


Patch:
diff --git a/docs/constraint_creation.md b/docs/constraint_creation.md
index 6a63711..c079a94 100644
--- a/docs/constraint_creation.md
+++ b/docs/constraint_creation.md
@@ -8,8 +8,8 @@ When using `konstraint doc`, Konstraint will create documentation for each polic
 
 The following rule names are organized into their own sections as they have special meaning within the context of Gatekeeper and Conftest:
 
-- `violation` (supported by Gatekeeper and Conftest)
-- `warn` (supported by Conftest)
+- `violation`
+- `warn`
 
 If a policy file does not contain any of the above rules, the policy is added to the `Other` section.
 
@@ -32,16 +32,16 @@ For example, a policy found in: `policies/pod-volume-size-limits/src.rego` gener
   - kind: _PodVolumeSizeLimits_
   - name: _podvolumesizelimits_
 
-When using the `--output` parameter, all templates and constraints will be generated in the path specified in the parameter with the format:
+When using the `--output` flag, all templates and constraints will be generated in the path specified in the parameter with the format:
 
 - constraint_PodVolumeSizeLimits.yaml
 - template_PodVolumeSizeLimits.yaml
 
 _NOTE: While not technically required, the tool works best with a folder structure similar to how Gatekeeper itself [structures policies and templates](https://github.com/open-policy-agent/gatekeeper-library/tree/master/library)._
 
-## Annotating Rules
+## Annotating Policies
 
-To further promote that the `.rego` file is the source of truth for policy, a block comment can be added to each policy file. Konstraint uses the [OPA Metadata Annotations](https://www.openpolicyagent.org/docs/latest/annotations/) to achieve this. The OPA metadata annotations are a YAML document in the comments above the package declaration preceded by a line containing the `METADATA` tag. Standard metadata fields are use where possible, but additional Gatekeeper-specific annotations are used under the `custom` metadata key as necessary.
+To further promote that the `.rego` file is the source of truth for policy, a block comment can be added to each policy file. Konstraint uses the [OPA Metadata Annotations](https://www.openpolicyagent.org/docs/latest/annotations/) to achieve this. The OPA metadata annotations are a YAML document in the comments above the package declaration preceded by a line containing the `METADATA` tag. Standard metadata fields are use where possible, but additional Gatekeeper-specific annotations are used under the `custom` metadata key as necessary. The metadata comment block is also what is used when generating documentation via the `doc` command.
 
 This comment block should:
 
@@ -49,7 +49,7 @@ This comment block should:
 - Include a human readable description of what the policy does.
 - Set the matchers used when generating the Constraints.
 
-It may also specify the enforcement action (either `deny` or `dryrun`) that Gatekeeper should take when a resource violates the constraint. If no enforcement action is specified, Konstraint defaults to using `deny` to align with Gatekeeper's default action. If the enforcement is set to `dryrun`, the policy will be skipped in the documentation generation.
+It may also specify the [enforcement action](https://open-policy-agent.github.io/gatekeeper/website/docs/howto/#the-enforcementaction-field) (`deny`, `warn`, or `dryrun`) that Gatekeeper should take when a resource violates the constraint. If no enforcement action is specified, Konstraint defaults to using `deny` to align with Gatekeeper's default action. If the enforcement is set to `dryrun`, the policy will be skipped in the documentation generation.
 
 ```rego
 # METADATA
@@ -75,11 +75,9 @@ pod_has_hostipc {
 }
 ```
 
-The comment block is also what is used when generating documentation via the `doc` command.
-
 ### Annotating rules for matchers
 
-Any matchers that Gatekeeper [supports](https://open-policy-agent.github.io/gatekeeper/website/docs/howto/#the-match-field) can be added under the `custom.matchers` annotation. These matchers are embedded into the `ConstraintTemplate` resource as-is. The example below will create a `ConstraintTemplate` that only applies to Kubernetes [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/) resources in namespaces named `foo`, `bar`, or `bar`.
+Any matchers that Gatekeeper [supports](https://open-policy-agent.github.io/gatekeeper/website/docs/howto/#the-match-field) can be added under the `custom.matchers` annotation. These matchers are embedded into the `ConstraintTemplate` resource as-is. The example below will create a `ConstraintTemplate` that only applies to Kubernetes [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/) resources in namespaces named `foo`, `bar`, or `baz`.
 
 ```rego
 # METADATA
@@ -109,6 +107,11 @@ violation[{"msg": msg}] {
 
 In some scenarios, you may wish for Konstraint to skip the generation of the `Constraint` resource for a policy and manage that externally. To do so, add the `skipConstraint: true` annotation in the custom metadata section.
 
+### Legacy annotations
+Previously Konstraint had custom annotation format, such as `@title` or `@kinds`, which is a legacy format and will be removed in future releases.
+
+To aid with transition to OPA Metadata format, a conversion tool is provided as `konstraint convert`
+
 ## Using Input Parameters
 
 Gatekeeper has the ability for a single `ConstraintTemplate` resource to be used by multiple `Constraint`s. One of the reasons for this is that it allows for passing input parameters to the policy so a single policy to avoid duplication. Konstraint supports these input parameters via the `parameters` object in the custom metadata section. **NOTE:** When input parameters are specified, Konstraint skips the generation of the `Constraint` resource unless the `--partial-constraint` flag is set.
diff --git a/examples/any-warn-deprecated-api-versions/src.rego b/examples/any-warn-deprecated-api-versions/src.rego
index dcdcfe2..e349953 100644
--- a/examples/any-warn-deprecated-api-versions/src.rego
+++ b/examples/any-warn-deprecated-api-versions/src.rego
@@ -1,10 +1,17 @@
-# @title Deprecated Deployment and DaemonSet API
-#
-# The `extensions/v1beta1 API` has been deprecated in favor of `apps/v1`. Later versions of Kubernetes
-# remove this API so to ensure that the Deployment or DaemonSet can be successfully deployed to the cluster,
-# the version for both of these resources must be `apps/v1`.
-#
-# @kinds apps/DaemonSet apps/Deployment
+# METADATA
+# title: Deprecated Deployment and DaemonSet API
+# description: |-
+#   The `extensions/v1beta1 API` has been deprecated in favor of `apps/v1`. Later versions of Kubernetes
+#   remove this API so to ensure that the Deployment or DaemonSet can be successfully deployed to the cluster,
+#   the version for both of these resources must be `apps/v1`.
+# custom:
+#   matchers:
+#     kinds:
+#     - apiGroups:
+#       - apps
+#       kinds:
+#       - DaemonSet
+#       - Deployment
 package any_warn_deprecated_api_versions
 
 policyID := "P0001"
diff --git a/examples/container-deny-added-caps/src.rego b/examples/container-deny-added-caps/src.rego
index 526b2a6..4c97f8a 100644
--- a/examples/container-deny-added-caps/src.rego
+++ b/examples/container-deny-added-caps/src.rego
@@ -1,10 +1,22 @@
-# @title Containers must drop all capabilities
-#
-# Granting containers privileged capabilities on the node makes it easier
-# for containers to escalate their privileges. As such, this is not allowed
-# outside of Kubernetes controller namespaces.
-#
-# @kinds apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+# METADATA
+# title: Containers must drop all capabilities
+# description: |-
+#   Granting containers privileged capabilities on the node makes it easier
+#   for containers to escalate their privileges. As such, this is not allowed
+#   outside of Kubernetes controller namespaces.
+# custom:
+#   matchers:
+#     kinds:
+#     - apiGroups:
+#       - ""
+#       kinds:
+#       - Pod
+#     - apiGroups:
+#       - apps
+#       kinds:
+#       - DaemonSet
+#       - Deployment
+#       - StatefulSet
 package container_deny_added_caps
 
 import data.lib.core
diff --git a/examples/container-deny-escalation/src.rego b/examples/container-deny-escalation/src.rego
index c5acd45..eb824b8 100644
--- a/examples/container-deny-escalation/src.rego
+++ b/examples/container-deny-escalation/src.rego
@@ -1,9 +1,21 @@
-# @title Containers must not allow for privilege escalation
-#
-# Privileged containers can much more easily obtain root on the node.
-# As such, they are not allowed.
-#
-# @kinds apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+# METADATA
+# title: Containers must not allow for privilege escalation
+# description: |-
+#   Privileged containers can much more easily obtain root on the node.
+#   As such, they are not allowed.
+# custom:
+#   matchers:
+#     kinds:
+#     - apiGroups:
+#       - ""
+#       kinds:
+#       - Pod
+#     - apiGroups:
+#       - apps
+#       kinds:
+#       - DaemonSet
+#       - Deployment
+#       - StatefulSet
 package container_deny_escalation
 
 import data.lib.core
diff --git a/examples/container-deny-privileged-if-tenant/src.rego b/examples/container-deny-privileged-if-tenant/src.rego
index 5c3cbaf..f4fa7ed 100644
--- a/examples/container-deny-privileged-if-tenant/src.rego
+++ b/examples/container-deny-privileged-if-tenant/src.rego
@@ -1,14 +1,27 @@
-# @title Tenants' containers must not run as privileged
-#
-# Privileged containers can easily escalate to root privileges on the node. As
-# such containers running as privileged or with sufficient capabilities granted
-# to obtain the same effect are not allowed if they are labeled as tenant.
-# To take advantage of this policy, it must be combined with another policy
-# that enforces the 'is-tenant' label.
-# This is the example for @matchlabels.
-#
-# @kinds apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
-# @matchlabels is-tenant=true
+# METADATA
+# title: Tenants' containers must not run as privileged
+# description: |-
+#   Privileged containers can easily escalate to root privileges on the node. As
+#   such containers running as privileged or with sufficient capabilities granted
+#   to obtain the same effect are not allowed if they are labeled as tenant.
+#   To take advantage of this policy, it must be combined with another policy
+#   that enforces the 'is-tenant' label.
+# custom:
+#   matchers:
+#     kinds:
+#     - apiGroups:
+#       - ""
+#       kinds:
+#       - Pod
+#     - apiGroups:
+#       - apps
+#       kinds:
+#       - DaemonSet
+#       - Deployment
+#       - StatefulSet
+#     labelSelector:
+#       matchLabels:
+#         is-tenant: "true"
 package container_deny_privileged_if_tenant
 
 import data.lib.core
diff --git a/examples/container-deny-privileged/src.rego b/examples/container-deny-privileged/src.rego
index c9ddfaa..ed0065e 100644
--- a/examples/container-deny-privileged/src.rego
+++ b/examples/container-deny-privileged/src.rego
@@ -1,10 +1,22 @@
-# @title Containers must not run as privileged
-#
-# Privileged containers can easily escalate to root privileges on the node. As
-# such containers running as privileged or with sufficient capabilities granted
-# to obtain the same effect are not allowed.
-#
-# @kinds apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+# METADATA
+# title: Containers must not run as privileged
+# description: |-
+#   Privileged containers can easily escalate to root privileges on the node. As
+#   such containers running as privileged or with sufficient capabilities granted
+#   to obtain the same effect are not allowed.
+# custom:
+#   matchers:
+#     kinds:
+#     - apiGroups:
+#       - ""
+#       kinds:
+#       - Pod
+#     - apiGroups:
+#       - apps
+#       kinds:
+#       - DaemonSet
+#       - Deployment
+#       - StatefulSet
 package container_deny_privileged
 
 import data.lib.core
diff --git a/examples/container-deny-without-resource-constraints/src.rego b/examples/container-deny-without-resource-constraints/src.rego
index 596e905..eeb9d8c 100644
--- a/examples/container-deny-without-resource-constraints/src.rego
+++ b/examples/container-deny-without-resource-constraints/src.rego
@@ -1,9 +1,21 @@
-# @title Containers must define resource constraints
-#
-# Resource constraints on containers ensure that a given workload does not take up more resources than it requires
-# and potentially starve other applications that need to run.
-#
-# @kinds apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+# METADATA
+# title: Containers must define resource constraints
+# description: |-
+#   Resource constraints on containers ensure that a given workload does not take up more resources than it requires
+#   and potentially starve other applications that need to run.
+# custom:
+#   matchers:
+#     kinds:
+#     - apiGroups:
+#       - ""
+#       kinds:
+#       - Pod
+#     - apiGroups:
+#       - apps
+#       kinds:
+#       - DaemonSet
+#       - Deployment
+#       - StatefulSet
 package container_deny_without_resource_constraints
 
 import data.lib.core
diff --git a/examples/container-warn-no-ro-fs/src.rego b/examples/container-warn-no-ro-fs/src.rego
index c2db588..73ead3b 100644
--- a/examples/container-warn-no-ro-fs/src.rego
+++ b/examples/container-warn-no-ro-fs/src.rego
@@ -1,9 +1,21 @@
-# @title Containers should not have a writable root filesystem
-#
-# In order to prevent persistence in the case of a compromise, it is
-# important to make the root filesystem read-only.
-#
-# @kinds apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+# METADATA
+# title: Containers should not have a writable root filesystem
+# description: |-
+#   In order to prevent persistence in the case of a compromise, it is
+#   important to make the root filesystem read-only.
+# custom:
+#   matchers:
+#     kinds:
+#     - apiGroups:
+#       - ""
+#       kinds:
+#       - Pod
+#     - apiGroups:
+#       - apps
+#       kinds:
+#       - DaemonSet
+#       - Deployment
+#       - StatefulSet
 package container_warn_no_ro_fs
 
 import data.lib.core
diff --git a/examples/pod-deny-host-ipc/src.rego b/examples/pod-deny-host-ipc/src.rego
index 7889600..d5ec740 100644
--- a/examples/pod-deny-host-ipc/src.rego
+++ b/examples/pod-deny-host-ipc/src.rego
@@ -1,9 +1,21 @@
-# @title Pods must not run with access to the host IPC
-#
-# Pods that are allowed to access the host IPC can read memory of
-# the other containers, breaking that security boundary.
-#
-# @kinds apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+# METADATA
+# title: Pods must not run with access to the host IPC
+# description: |-
+#   Pods that are allowed to access the host IPC can read memory of
+#   the other containers, breaking that security boundary.
+# custom:
+#   matchers:
+#     kinds:
+#     - apiGroups:
+#       - ""
+#       kinds:
+#       - Pod
+#     - apiGroups:
+#       - apps
+#       kinds:
+#       - DaemonSet
+#       - Deployment
+#       - StatefulSet
 package pod_deny_host_ipc
 
 import data.lib.core
diff --git a/examples/pod-deny-host-network/src.rego b/examples/pod-deny-host-network/src.rego
index c3c3252..7d17dcc 100644
--- a/examples/pod-deny-host-network/src.rego
+++ b/examples/pod-deny-host-network/src.rego
@@ -1,9 +1,21 @@
-# @title Pods must not run with access to the host networking
-#
-# Pods that can access the host's network interfaces can potentially
-# access and tamper with traffic the pod should not have access to.
-#
-# @kinds apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+# METADATA
+# title: Pods must not run with access to the host networking
+# description: |-
+#   Pods that can access the host's network interfaces can potentially
+#   access and tamper with traffic the pod should not have access to.
+# custom:
+#   matchers:
+#     kinds:
+#     - apiGroups:
+#       - ""
+#       kinds:
+#       - Pod
+#     - apiGroups:
+#       - apps
+#       kinds:
+#       - DaemonSet
+#       - Deployment
+#       - StatefulSet
 package pod_deny_host_network
 
 import data.lib.core
diff --git a/examples/pod-deny-host-pid/src.rego b/examples/pod-deny-host-pid/src.rego
index c32a715..f79c783 100644
--- a/examples/pod-deny-host-pid/src.rego
+++ b/examples/pod-deny-host-pid/src.rego
@@ -1,10 +1,22 @@
-# @title Pods must not run with access to the host PID namespace
-#
-# Pods that can access the host's process tree can view and attempt to
-# modify processes outside of their namespace, breaking that security
-# boundary.
-#
-# @kinds apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+# METADATA
+# title: Pods must not run with access to the host PID namespace
+# description: |-
+#   Pods that can access the host's process tree can view and attempt to
+#   modify processes outside of their namespace, breaking that security
+#   boundary.
+# custom:
+#   matchers:
+#     kinds:
+#     - apiGroups:
+#       - ""
+#       kinds:
+#       - Pod
+#     - apiGroups:
+#       - apps
+#       kinds:
+#       - DaemonSet
+#       - Deployment
+#       - StatefulSet
 package pod_deny_host_pid
 
 import data.lib.core
diff --git a/examples/pod-deny-without-runasnonroot/src.rego b/examples/pod-deny-without-runasnonroot/src.rego
index e534dcd..66d1f9a 100644
--- a/examples/pod-deny-without-runasnonroot/src.rego
+++ b/examples/pod-deny-without-runasnonroot/src.rego
@@ -1,9 +1,21 @@
-# @title Pods must run as non-root
-#
-# Pods running as root (uid of 0) can much more easily escalate privileges
-# to root on the node. As such, they are not allowed.
-#
-# @kinds apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+# METADATA
+# title: Pods must run as non-root
+# description: |-
+#   Pods running as root (uid of 0) can much more easily escalate privileges
+#   to root on the node. As such, they are not allowed.
+# custom:
+#   matchers:
+#     kinds:
+#     - apiGroups:
+#       - ""
+#       kinds:
+#       - Pod
+#     - apiGroups:
+#       - apps
+#       kinds:
+#       - DaemonSet
+#       - Deployment
+#       - StatefulSet
 package pod_deny_without_runasnonroot
 
 import data.lib.core
diff --git a/examples/policies-no-rego.md b/examples/policies-no-rego.md
index bd1eae9..3f78e08 100644
--- a/examples/policies-no-rego.md
+++ b/examples/policies-no-rego.md
@@ -293,7 +293,6 @@ such containers running as privileged or with sufficient capabilities granted
 to obtain the same effect are not allowed if they are labeled as tenant.
 To take advantage of this policy, it must be combined with another policy
 that enforces the 'is-tenant' label.
-This is the example for @matchlabels.
 
 
 _source: [container-deny-privileged-if-tenant](container-deny-privileged-if-tenant)_
diff --git a/examples/policies.md b/examples/policies.md
index 9557a27..ad68577 100755
--- a/examples/policies.md
+++ b/examples/policies.md
@@ -724,7 +724,6 @@ such containers running as privileged or with sufficient capabilities granted
 to obtain the same effect are not allowed if they are labeled as tenant.
 To take advantage of this policy, it must be combined with another policy
 that enforces the 'is-tenant' label.
-This is the example for @matchlabels.
 
 ### Rego
 
diff --git a/examples/psp-deny-added-caps/src.rego b/examples/psp-deny-added-caps/src.rego
index 94bc00f..4e1ad7a 100644
--- a/examples/psp-deny-added-caps/src.rego
+++ b/examples/psp-deny-added-caps/src.rego
@@ -1,10 +1,16 @@
-# @title PodSecurityPolicies must require all capabilities are dropped
-# 
-# Allowing containers privileged capabilities on the node makes it easier
-# for containers to escalate their privileges. As such, this is not allowed
-# outside of Kubernetes controller namespaces.
-#
-# @kinds policy/PodSecurityPolicy
+# METADATA
+# title: PodSecurityPolicies must require all capabilities are dropped
+# description: |-
+#   Allowing containers privileged capabilities on the node makes it easier
+#   for containers to escalate their privileges. As such, this is not allowed
+#   outside of Kubernetes controller namespaces.
+# custom:
+#   matchers:
+#     kinds:
+#     - apiGroups:
+#       - policy
+#       kinds:
+#       - PodSecurityPolicy
 package psp_deny_added_caps
 
 import data.lib.core
diff --git a/examples/psp-deny-escalation/src.rego b/examples/psp-deny-escalation/src.rego
index be5aac7..69ecb95 100644
--- a/examples/psp-deny-escalation/src.rego
+++ b/examples/psp-deny-escalation/src.rego
@@ -1,9 +1,15 @@
-# @title PodSecurityPolicies must not allow privileged escalation
-# 
-# Allowing privileged containers can much more easily obtain root on the node.
-# As such, they are not allowed.
-#
-# @kinds policy/PodSecurityPolicy
+# METADATA
+# title: PodSecurityPolicies must not allow privileged escalation
+# description: |-
+#   Allowing privileged containers can much more easily obtain root on the node.
+#   As such, they are not allowed.
+# custom:
+#   matchers:
+#     kinds:
+#     - apiGroups:
+#       - policy
+#       kinds:
+#       - PodSecurityPolicy
 package psp_deny_escalation
 
 import data.lib.core
diff --git a/examples/psp-deny-host-alias/src.rego b/examples/psp-deny-host-alias/src.rego
index 2d85585..64db925 100644
--- a/examples/psp-deny-host-alias/src.rego
+++ b/examples/psp-deny-host-alias/src.rego
@@ -1,9 +1,15 @@
-# @title PodSecurityPolicies must not allow access to the host aliases
-# 
-# Allowing pods to can change aliases in the host's /etc/hosts file can 
-# redirect traffic to malicious servers.
-#
-# @kinds policy/PodSecurityPolicy
+# METADATA
+# title: PodSecurityPolicies must not allow access to the host aliases
+# description: |-
+#   Allowing pods to can change aliases in the host's /etc/hosts file can
+#   redirect traffic to malicious servers.
+# custom:
+#   matchers:
+#     kinds:
+#     - apiGroups:
+#       - policy
+#       kinds:
+#       - PodSecurityPolicy
 package psp_deny_host_alias
 
 import data.lib.core
diff --git a/examples/psp-deny-host-ipc/src.rego b/examples/psp-deny-host-ipc/src.rego
index c803c00..9883217 100644
--- a/examples/psp-deny-host-ipc/src.rego
+++ b/examples/psp-deny-host-ipc/src.rego
@@ -1,9 +1,15 @@
-# @title PodSecurityPolicies must not allow access to the host IPC
-#
-# Allowing pods to access the host IPC can read memory of
-# the other containers, breaking that security boundary.
-#
-# @kinds policy/PodSecurityPolicy
+# METADATA
+# title: PodSecurityPolicies must not allow access to the host IPC
+# description: |-
+#   Allowing pods to access the host IPC can read memory of
+#   the other containers, breaking that security boundary.
+# custom:
+#   matchers:
+#     kinds:
+#     - apiGroups:
+#       - policy
+#       kinds:
+#       - PodSecurityPolicy
 package psp_deny_host_ipc
 
 import data.lib.core
diff --git a/examples/psp-deny-host-network/src.rego b/examples/psp-deny-host-network/src.rego
index 0da904e..ab5df1d 100644
--- a/examples/psp-deny-host-network/src.rego
+++ b/examples/psp-deny-host-network/src.rego
@@ -1,10 +1,16 @@
-# @title PodSecurityPolicies must not allow access to the host network
-#
-# Allowing pods to access the host's process tree can view and attempt to
-# modify processes outside of their namespace, breaking that security
-# boundary.
-#
-# @kinds policy/PodSecurityPolicy
+# METADATA
+# title: PodSecurityPolicies must not allow access to the host network
+# description: |-
+#   Allowing pods to access the host's process tree can view and attempt to
+#   modify processes outside of their namespace, breaking that security
+#   boundary.
+# custom:
+#   matchers:
+#     kinds:
+#     - apiGroups:
+#       - policy
+#       kinds:
+#       - PodSecurityPolicy
 package psp_deny_host_network
 
 import data.lib.core
diff --git a/examples/psp-deny-host-pid/src.rego b/examples/psp-deny-host-pid/src.rego
index 42b5c52..d8d706a 100644
--- a/examples/psp-deny-host-pid/src.rego
+++ b/examples/psp-deny-host-pid/src.rego
@@ -1,10 +1,16 @@
-# @title PodSecurityPolicies must not allow access to the host PID namespace
-#
-# Allowing pods to access the host's process tree can view and attempt to
-# modify processes outside of their namespace, breaking that security
-# boundary.
-#
-# @kinds policy/PodSecurityPolicy
+# METADATA
+# title: PodSecurityPolicies must not allow access to the host PID namespace
+# description: |-
+#   Allowing pods to access the host's process tree can view and attempt to
+#   modify processes outside of their namespace, breaking that security
+#   boundary.
+# custom:
+#   matchers:
+#     kinds:
+#     - apiGroups:
+#       - policy
+#       kinds:
+#       - PodSecurityPolicy
 package psp_deny_host_pid
 
 import data.lib.core
diff --git a/examples/psp-deny-privileged/src.rego b/examples/psp-deny-privileged/src.rego
index 3747b02..dec6fc1 100644
--- a/examples/psp-deny-privileged/src.rego
+++ b/examples/psp-deny-privileged/src.rego
@@ -1,9 +1,15 @@
-# @title PodSecurityPolicies must require containers to not run as privileged
-#
-# Allowing privileged containers can much more easily obtain root on the node.
-# As such, they are not allowed.
-#
-# @kinds policy/PodSecurityPolicy
+# METADATA
+# title: PodSecurityPolicies must require containers to not run as privileged
+# description: |-
+#   Allowing privileged containers can much more easily obtain root on the node.
+#   As such, they are not allowed.
+# custom:
+#   matchers:
+#     kinds:
+#     - apiGroups:
+#       - policy
+#       kinds:
+#       - PodSecurityPolicy
 package psp_deny_privileged
 
 import data.lib.core
diff --git a/examples/psp-warn-no-ro-fs/src.rego b/examples/psp-warn-no-ro-fs/src.rego
index 8cc09a9..7e08627 100644
--- a/examples/psp-warn-no-ro-fs/src.rego
+++ b/examples/psp-warn-no-ro-fs/src.rego
@@ -1,9 +1,15 @@
-# @title PodSecurityPolicies should require that a read-only root filesystem is set
-#
-# Allowing pods to access the host's network interfaces can potentially
-# access and tamper with traffic the pod should not have access to.
-#
-# @kinds policy/PodSecurityPolicy
+# METADATA
+# title: PodSecurityPolicies should require that a read-only root filesystem is set
+# description: |-
+#   Allowing pods to access the host's network interfaces can potentially
+#   access and tamper with traffic the pod should not have access to.
+# custom:
+#   matchers:
+#     kinds:
+#     - apiGroups:
+#       - policy
+#       kinds:
+#       - PodSecurityPolicy
 package psp_warn_no_ro_fs
 
 import data.lib.core
diff --git a/examples/role-deny-use-privileged-psp/src.rego b/examples/role-deny-use-privileged-psp/src.rego
index 1d1eaaf..1cdfb4c 100644
--- a/examples/role-deny-use-privileged-psp/src.rego
+++ b/examples/role-deny-use-privileged-psp/src.rego
@@ -1,8 +1,14 @@
-# @title Roles must not allow use of privileged PodSecurityPolicies
-# 
-# Workloads not running in the exempted namespaces must not use PodSecurityPolicies with privileged permissions.
-#
-# @kinds rbac.authorization.k8s.io/Role
+# METADATA
+# title: Roles must not allow use of privileged PodSecurityPolicies
+# description: Workloads not running in the exempted namespaces must not use PodSecurityPolicies
+#   with privileged permissions.
+# custom:
+#   matchers:
+#     kinds:
+#     - apiGroups:
+#       - rbac.authorization.k8s.io
+#       kinds:
+#       - Role
 package role_deny_use_privileged_psps
 
 import data.lib.core
diff --git a/internal/commands/convert.go b/internal/commands/convert.go
new file mode 100644
index 0000000..19c9ad5
--- /dev/null
+++ b/internal/commands/convert.go
@@ -0,0 +1,123 @@
+package commands
+
+import (
+	"fmt"
+	"io/ioutil"
+	"strings"
+
+	"github.com/ghodss/yaml"
+	"github.com/plexsystems/konstraint/internal/rego"
+	log "github.com/sirupsen/logrus"
+	"github.com/spf13/cobra"
+)
+
+func newConvertCommand() *cobra.Command {
+	cmd := cobra.Command{
+		Use:   "convert <dir>",
+		Short: "Convert legacy annotations to OPA Metadata Annotations",
+		Long:  "Converts legacy annotations to OPA Metadata Annotations. Policies that already have any package-level OPA Metadata Annotations set will be skipped, even if the policy contains legacy annotations.",
+		Example: `Convert all policies with legacy annotations to OPA Metadata annotations in-place
+
+konstraint convert examples`,
+
+		RunE: func(cmd *cobra.Command, args []string) error {
+			path := "."
+			if len(args) > 0 {
+				path = args[0]
+			}
+
+			return runConvertCommand(path)
+		},
+	}
+
+	return &cmd
+}
+
+func runConvertCommand(path string) error {
+	regos, err := rego.GetAllSeveritiesWithoutImports(path)
+	if err != nil {
+		return fmt.Errorf("get regos: %w", err)
+	}
+
+	var conversions int
+
+	for _, r := range regos {
+		logger := log.WithFields(log.Fields{
+			"name": r.Kind(),
+			"src":  r.Path(),
+		})
+
+		if r.HasMetadataAnnotations() {
+			logger.Info("was skipped since it has OPA Annotations already")
+			continue
+		}
+
+		conveted, err := r.ConvertLegacyAnnotations()
+		if err != nil {
+			logger.WithError(err).Error("Failed to convert legacy annotations")
+		}
+
+		var sb strings.Builder
+		sb.WriteString("# METADATA\n")
+
+		// force order: `title`, `description`, `custom`
+		if conveted.Title != "" {
+			yml, err := yaml.Marshal(&rego.ConvertedLegacyAnnotations{Title: conveted.Title})
+			if err != nil {
+				logger.WithError(err).Error("Failed to marshal OPA Annotations title field to YAML")
+				continue
+			}
+			appendCommentedYaml(&sb, yml)
+		}
+		if conveted.Description != "" {
+			yml, err := yaml.Marshal(&rego.ConvertedLegacyAnnotations{Description: conveted.Description})
+			if err != nil {
+				logger.WithError(err).Error("Failed to marshal OPA Annotations description field to YAML")
+				continue
+			}
+			appendCommentedYaml(&sb, yml)
+		}
+		if len(conveted.Custom) > 0 {
+			yml, err := yaml.Marshal(&rego.ConvertedLegacyAnnotations{Custom: conveted.Custom})
+			if err != nil {
+				logger.WithError(err).Error("Failed to marshal OPA Annotations custom field to YAML")
+				continue
+			}
+			appendCommentedYaml(&sb, yml)
+		}
+
+		sb.WriteString(r.LegacyConversionSource())
+		sb.WriteByte('\n')
+
+		if err := ioutil.WriteFile(r.Path(), []byte(sb.String()), 0644); err != nil {
+			return fmt.Errorf("writing updated policy source: %w", err)
+		}
+
+		conversions++
+		logger.Info("converted successfully")
+	}
+
+	log.WithFields(log.Fields{
+		"num_policies":  len(regos),
+		"num_converted": conversions,
+	}).Info("Completed successfully.")
+
+	return nil
+}
+
+func appendCommentedYaml(sb *strings.Builder, yaml []byte) {
+	split := strings.Split(string(yaml), "\n")
+
+	for i, line := range split {
+		// avoid trailing spaces
+		if line == "" {
+			// skip last line if it's empty
+			if i != len(split)-1 {
+				sb.WriteString("#\n")
+			}
+			continue
+		}
+
+		sb.WriteString("# " + line + "\n")
+	}
+}
diff --git a/internal/commands/create.go b/internal/commands/create.go
index a1d83c4..b35fe6e 100644
--- a/internal/commands/create.go
+++ b/internal/commands/create.go
@@ -21,6 +21,10 @@ import (
 	"k8s.io/apimachinery/pkg/runtime/schema"
 )
 
+const (
+	legacyMigrationMessage = " are set with legacy annotations, this functionality will be removed in a future release. Please migrate to OPA Metadata annotations. See konstraint convert."
+)
+
 func newCreateCommand() *cobra.Command {
 	cmd := cobra.Command{
 		Use:   "create <dir>",
@@ -126,7 +130,7 @@ func runCreateCommand(path string) error {
 		}
 
 		// Skip Constraint generation if there are parameters on the template.
-		if len(violation.Parameters()) > 0 && !viper.GetBool("partial-constraints") {
+		if !viper.GetBool("partial-constraints") && (len(violation.Parameters()) > 0 || len(violation.AnnotationParameters()) > 0) {
 			logger.Warn("Skipping constraint generation due to use of parameters")
 			continue
 		}
@@ -179,10 +183,10 @@ func getConstraintTemplatev1(violation rego.Rego, logger *log.Entry) (*v1.Constr
 	}
 
 	if len(violation.Parameters()) > 0 {
-		logger.Warn("Parameters are set with legacy annotations, this functionality will be removed in a future release. Please migrate to OPA Metadata annotations.")
+		logger.Warn("Parameters" + legacyMigrationMessage)
 		constraintTemplate.Spec.CRD.Spec.Validation = &v1.Validation{
 			OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
-				Properties: getOpenAPISchemaProperties(violation),
+				Properties: violation.GetOpenAPISchemaProperties(),
 				Type:       "object",
 			},
 		}
@@ -231,10 +235,10 @@ func getConstraintTemplatev1beta1(violation rego.Rego, logger *log.Entry) (*v1be
 	}
 
 	if len(violation.Parameters()) > 0 {
-		logger.Warn("Parameters are set with legacy annotations, this functionality will be removed in a future release. Please migrate to OPA Metadata annotations.")
+		logger.Warn("Parameters" + legacyMigrationMessage)
 		constraintTemplate.Spec.CRD.Spec.Validation = &v1beta1.Validation{
 			OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
-				Properties: getOpenAPISchemaProperties(violation),
+				Properties: violation.GetOpenAPISchemaProperties(),
 			},
 		}
 	}
@@ -253,28 +257,6 @@ func getConstraintTemplatev1beta1(violation rego.Rego, logger *log.Entry) (*v1be
 	return &constraintTemplate, nil
 }
 
-func getOpenAPISchemaProperties(r rego.Rego) map[string]apiextensionsv1.JSONSchemaProps {
-	properties := make(map[string]apiextensionsv1.JSONSchemaProps)
-	for _, p := range r.Parameters() {
-		if p.IsArray {
-			properties[p.Name] = apiextensionsv1.JSONSchemaProps{
-				Type:        "array",
-				Description: p.Description,
-				Items: &apiextensionsv1.JSONSchemaPropsOrArray{
-					Schema: &apiextensionsv1.JSONSchemaProps{Type: p.Type},
-				},
-			}
-		} else {
-			properties[p.Name] = apiextensionsv1.JSONSchemaProps{
-				Type:        p.Type,
-				Description: p.Description,
-			}
-		}
-	}
-
-	return properties
-}
-
 func getConstraint(violation rego.Rego, logger *log.Entry) (*unstructured.Unstructured, error) {
 	gvk := schema.GroupVersionKind{
 		Group:   "constraints.gatekeeper.sh",
@@ -306,35 +288,35 @@ func getConstraint(violation rego.Rego, logger *log.Entry) (*unstructured.Unstru
 	}
 
 	if len(matchers.KindMatchers) > 0 {
-		logger.Warn("Kind Matchers are set with legacy annotations, this functionality will be removed in a future release. Please migrate to OPA Metadata annotations.")
+		logger.Warn("Kind Matchers" + legacyMigrationMessage)
 		if err := setKindMatcher(&constraint, matchers.KindMatchers); err != nil {
 			return nil, fmt.Errorf("set kind matcher: %w", err)
 		}
 	}
 
 	if len(matchers.MatchLabelsMatcher) > 0 {
-		logger.Warn("Match Labels Matchers are set with legacy annotations, this functionality will be removed in a future release. Please migrate to OPA Metadata annotations.")
+		logger.Warn("Match Labels Matchers" + legacyMigrationMessage)
 		if err := setMatchLabelsMatcher(&constraint, matchers.MatchLabelsMatcher); err != nil {
 			return nil, fmt.Errorf("set match labels matcher: %w", err)
 		}
 	}
 
 	if len(matchers.MatchExpressionsMatcher) > 0 {
-		logger.Warn("Match Expressions Matchers are set with legacy annotations, this functionality will be removed in a future release. Please migrate to OPA Metadata annotations.")
+		logger.Warn("Match Expressions Matchers" + legacyMigrationMessage)
 		if err := setMatchExpressionsMatcher(&constraint, matchers.MatchExpressionsMatcher); err != nil {
 			return nil, fmt.Errorf("set match expressions matcher: %w", err)
 		}
 	}
 
 	if len(matchers.NamespaceMatcher) > 0 {
-		logger.Warn("Namespace Matchers are set with legacy annotations, this functionality will be removed in a future release. Please migrate to OPA Metadata annotations.")
+		logger.Warn("Namespace Matchers" + legacyMigrationMessage)
 		if err := setNestedStringSlice(&constraint, matchers.NamespaceMatcher, "spec", "match", "namespaces"); err != nil {
 			return nil, fmt.Errorf("set namespace matcher: %w", err)
 		}
 	}
 
 	if len(matchers.ExcludedNamespaceMatcher) > 0 {
-		logger.Warn("Excluded Namespace Matchers are set with legacy annotations, this functionality will be removed in a future release. Please migrate to OPA Metadata annotations.")
+		logger.Warn("Excluded Namespace Matchers" + legacyMigrationMessage)
 		if err := setNestedStringSlice(&constraint, matchers.ExcludedNamespaceMatcher, "spec", "match", "excludedNamespaces"); err != nil {
 			return nil, fmt.Errorf("set namespace matcher: %w", err)
 		}
@@ -357,7 +339,7 @@ func getConstraint(violation rego.Rego, logger *log.Entry) (*unstructured.Unstru
 
 	if viper.GetBool("partial-constraints") {
 		if len(violation.Parameters()) > 0 {
-			logger.Warn("Parameters are set with legacy annotations, this functionality will be removed in a future release. Please migrate to OPA Metadata annotations.")
+			logger.Warn("Parameters" + legacyMigrationMessage)
 			if err := addParametersToConstraintLegacy(&constraint, violation.Parameters()); err != nil {
 				return nil, fmt.Errorf("add parameters %v to constraint: %w", violation.Parameters(), err)
 			}
@@ -397,29 +379,12 @@ func addParametersToConstraintLegacy(constraint *unstructured.Unstructured, para
 }
 
 func setKindMatcher(constraint *unstructured.Unstructured, kindMatchers rego.KindMatchers) error {
-	constraintMatchers := make([]interface{}, len(kindMatchers))
-
-	for i, matcher := range kindMatchers {
-		constraintMatchers[i] = map[string]interface{}{
-			"apiGroups": toInterfaceSlice([]string{matcher.APIGroup}),
-			"kinds":     toInterfaceSlice(matcher.Kinds),
-		}
-	}
-
-	if err := unstructured.SetNestedSlice(constraint.Object, constraintMatchers, "spec", "match", "kinds"); err != nil {
+	if err := unstructured.SetNestedSlice(constraint.Object, kindMatchers.ToSpec(), "spec", "match", "kinds"); err != nil {
 		return fmt.Errorf("set constraint kinds matchers: %w", err)
 	}
 	return nil
 }
 
-func toInterfaceSlice(input []string) []interface{} {
-	res := make([]interface{}, len(input))
-	for i, v := range input {
-		res[i] = v
-	}
-	return res
-}
-
 func setMatchLabelsMatcher(constraint *unstructured.Unstructured, matcher rego.MatchLabelsMatcher) error {
 	if err := unstructured.SetNestedStringMap(constraint.Object, matcher, "spec", "match", "labelSelector", "matchLabels"); err != nil {
 		return fmt.Errorf("set constraint labelSelector.matchLabels matchers: %w", err)
diff --git a/internal/commands/default.go b/internal/commands/default.go
index 37e4e13..415beb9 100644
--- a/internal/commands/default.go
+++ b/internal/commands/default.go
@@ -24,6 +24,7 @@ func NewDefaultCommand() *cobra.Command {
 
 	cmd.AddCommand(newCreateCommand())
 	cmd.AddCommand(newDocCommand())
+	cmd.AddCommand(newConvertCommand())
 
 	return &cmd
 }
diff --git a/internal/rego/matchers.go b/internal/rego/matchers.go
index e73b0e1..191bab8 100644
--- a/internal/rego/matchers.go
+++ b/internal/rego/matchers.go
@@ -49,6 +49,27 @@ func (k KindMatchers) String() string {
 	return result
 }
 
+// ToSpec converts KindMatchers to a slice in format
+// compatible with `spec.match.kinds` of a Constraint
+func (k KindMatchers) ToSpec() []any {
+	specSlice := make([]any, len(k))
+
+	for i, matcher := range k {
+		// convert to interface slice so apimachinery's runtime.DeepCopy works
+		kinds := make([]any, len(matcher.Kinds))
+		for j, kind := range matcher.Kinds {
+			kinds[j] = any(kind)
+		}
+
+		specSlice[i] = map[string]any{
+			"apiGroups": []any{any(matcher.APIGroup)},
+			"kinds":     kinds,
+		}
+	}
+
+	return specSlice
+}
+
 // addIfNotPresent adds an apiGroup/kind matcher to the map
 // unless it's already present
 //
diff --git a/internal/rego/rego.go b/internal/rego/rego.go
index b5042f1..edac1ee 100644
--- a/internal/rego/rego.go
+++ b/internal/rego/rego.go
@@ -31,11 +31,20 @@ const (
 	PolicyIDVariable = "policyID"
 )
 
+// Keys used in Custom section of OPA Rich Metadata Annotations
+const (
+	annoEnforcement    = "enforcement"
+	annoMatchers       = "matchers"
+	annoParameters     = "parameters"
+	annoSkipConstraint = "skipConstraint"
+)
+
 // Rego represents a parsed rego file.
 type Rego struct {
 	id             string
 	path           string
 	raw            string
+	sanitizedRaw   string
 	headerComments []string
 	rules          []string
 	dependencies   []string
@@ -189,21 +198,21 @@ func (r *Rego) parseAnnotations(annotations *ast.Annotations) error {
 		r.annoDescription = annotations.Description
 	}
 
-	matchers, ok := annotations.Custom["matchers"]
+	matchers, ok := annotations.Custom[annoMatchers]
 	if ok {
 		if err := r.parseAnnotationsMatchers(matchers.(map[string]any)); err != nil {
 			return fmt.Errorf("parse matchers from OPA metadata: %w", err)
 		}
 	}
 
-	parameters, ok := annotations.Custom["parameters"]
+	parameters, ok := annotations.Custom[annoParameters]
 	if ok {
 		if err := r.parseAnnotationsParameters(parameters.(map[string]any)); err != nil {
 			return fmt.Errorf("parse parameters from OPA metadata: %w", err)
 		}
 	}
 
-	skipConstraint, ok := annotations.Custom["skipConstraint"]
+	skipConstraint, ok := annotations.Custom[annoSkipConstraint]
 	if ok {
 		sc, ok := skipConstraint.(bool)
 		if !ok {
@@ -212,7 +221,7 @@ func (r *Rego) parseAnnotations(annotations *ast.Annotations) error {
 		r.skipConstraint = sc
 	}
 
-	enforcement, ok := annotations.Custom["enforcement"]
+	enforcement, ok := annotations.Custom[annoEnforcement]
 	if ok {
 		e, ok := enforcement.(string)
 		if !ok {
@@ -349,9 +358,12 @@ func (r Rego) Enforcement() string {
 	if r.enforcement != "" {
 		return r.enforcement
 	}
+	return "deny"
+}
 
-	enforcement := "deny"
-	for _, comment := range r.headerComments {
+func getEnforcementTag(headerComments []string) string {
+	enforcement := ""
+	for _, comment := range headerComments {
 		if !commentStartsWith(comment, "@enforcement") {
 			continue
 		}
@@ -418,20 +430,116 @@ func (r Rego) Description() string {
 	return description
 }
 
+// HasMetadataAnnotations checks whenether rego file has
+// OPA Metadata Annotations
+func (r Rego) HasMetadataAnnotations() bool {
+	return r.annotations != nil
+}
+
+// ConvertedLegacyAnnotations holds OPA Metadata Annotations,
+// which were converted from legacy style annotations
+type ConvertedLegacyAnnotations struct {
+	Title       string         `json:"title,omitempty"`
+	Description string         `json:"description,omitempty"`
+	Custom      map[string]any `json:"custom,omitempty"`
+}
+
+// ConvertLegacyAnnotations converts legacy annotations to ConvertedLegacyAnnotations
+func (r Rego) ConvertLegacyAnnotations() (*ConvertedLegacyAnnotations, error) {
+	custom := make(map[string]any)
+	if r.enforcement != "" {
+		custom[annoEnforcement] = r.enforcement
+	}
+	if r.skipConstraint {
+		custom[annoSkipConstraint] = r.skipConstraint
+	}
+	if len(r.parameters) > 0 {
+		custom[annoParameters] = r.GetOpenAPISchemaProperties()
+	}
+
+	matchers, err := r.Matchers()
+	if err != nil {
+		return nil, fmt.Errorf("cant get legacy matchers: %w", err)
+	}
+
+	matcherMap := make(map[string]any)
+
+	if len(matchers.KindMatchers) > 0 {
+		matcherMap["kinds"] = matchers.KindMatchers.ToSpec()
+	}
+
+	labelSelector := make(map[string]any)
+	if len(matchers.MatchLabelsMatcher) > 0 {
+		labelSelector["matchLabels"] = matchers.MatchLabelsMatcher
+	}
+	if len(matchers.MatchExpressionsMatcher) > 0 {
+		labelSelector["matchExpressions"] = matchers.MatchExpressionsMatcher
+	}
+	if len(labelSelector) > 0 {
+		matcherMap["labelSelector"] = labelSelector
+	}
+
+	if len(matchers.NamespaceMatcher) > 0 {
+		matcherMap["namespaces"] = matchers.NamespaceMatcher
+	}
+	if len(matchers.ExcludedNamespaceMatcher) > 0 {
+		matcherMap["excludedNamespaces"] = matchers.ExcludedNamespaceMatcher
+	}
+
+	if len(matcherMap) > 0 {
+		custom[annoMatchers] = matcherMap
+	}
+
+	return &ConvertedLegacyAnnotations{
+		Title:       r.Title(),
+		Description: r.Description(),
+		Custom:      custom,
+	}, nil
+}
+
+func (r Rego) GetOpenAPISchemaProperties() map[string]apiextensionsv1.JSONSchemaProps {
+	properties := make(map[string]apiextensionsv1.JSONSchemaProps)
+	for _, p := range r.Parameters() {
+		if p.IsArray {
+			properties[p.Name] = apiextensionsv1.JSONSchemaProps{
+				Type:        "array",
+				Description: p.Description,
+				Items: &apiextensionsv1.JSONSchemaPropsOrArray{
+					Schema: &apiextensionsv1.JSONSchemaProps{Type: p.Type},
+				},
+			}
+		} else {
+			properties[p.Name] = apiextensionsv1.JSONSchemaProps{
+				Type:        p.Type,
+				Description: p.Description,
+			}
+		}
+	}
+
+	return properties
+}
+
 // Source returns the original source code inside
 // of the rego file without any comments.
 func (r Rego) Source() string {
-	return removeComments(r.raw)
+	return removeComments(r.sanitizedRaw)
 }
 
 // FullSource returns the original source code inside
 // of the rego file including comments except the header
 func (r Rego) FullSource() string {
-	withoutHeader := removeHeaderComments(r.raw)
+	withoutHeader := removeHeaderComments(r.sanitizedRaw)
 
 	return strings.Trim(withoutHeader, "\n\t ")
 }
 
+// LegacyConversionSource returns the original source code
+// with comments except header,
+// but doesn't trim any trailing whitespace
+func (r Rego) LegacyConversionSource() string {
+	return removeHeaderComments(r.raw)
+}
+
 func removeHeaderComments(input string) string {
 	var result string
 	split := strings.Split(input, "\n")
@@ -481,11 +589,6 @@ func parseDirectory(directory string, parseImports bool) ([]Rego, error) {
 
 	files := make(map[string]*loader.RegoFile)
 	for m := range result.Modules {
-		// Many YAML parsers have problems handling carriage returns and tabs so we sanitize the Rego
-		// before storing it so it can be rendered properly.
-		result.Modules[m].Raw = bytes.ReplaceAll(result.Modules[m].Raw, []byte("\r"), []byte(""))
-		result.Modules[m].Raw = bytes.ReplaceAll(result.Modules[m].Raw, []byte("\t"), []byte("  "))
-
 		// Re-key the loaded rego file map based on the package path of the rego file.
 		// This makes finding the source rego file from an import path much easier.
 		files[result.Modules[m].Parsed.Package.Path.String()] = result.Modules[m]
@@ -504,7 +607,7 @@ func parseDirectory(directory string, parseImports bool) ([]Rego, error) {
 
 		var dependencies []string
 		for _, importPath := range importPaths {
-			dependencies = append(dependencies, removeComments(string(files[importPath].Raw)))
+			dependencies = append(dependencies, removeComments(sanitizeRawSource(files[importPath].Raw)))
 		}
 
 		var rules []string
@@ -533,33 +636,22 @@ func parseDirectory(directory string, parseImports bool) ([]Rego, error) {
 		}
 
 		bodyParams := getRuleParamNames(file.Parsed.Rules)
+		var paramsDiff []string
+
 		var headerParams []Parameter
-		if annotations != nil {
-			headerParams = getHeaderParams(annotations)
-		} else {
+		if annotations == nil {
 			headerParams, err = getHeaderParamsLegacy(headerComments)
 			if err != nil {
 				return nil, fmt.Errorf("parse header parameters: %w", err)
 			}
+			paramsDiff = paramDiff(bodyParams, headerParams)
+		} else {
+			tempHeaderParams := getHeaderParams(annotations)
+			paramsDiff = paramDiff(bodyParams, tempHeaderParams)
 		}
 
-		paramsDiff := paramDiff(bodyParams, headerParams)
 		if len(paramsDiff) > 0 {
-			return nil, fmt.Errorf("missing @parameter tags for parameters %v found in the policy: %v", paramsDiff, file.Name)
-		}
-
-		for _, bodyParam := range bodyParams {
-			var seen bool
-			for _, headerParam := range headerParams {
-				if headerParam.Name == bodyParam {
-					seen = true
-					break
-				}
-			}
-
-			if !seen {
-				return nil, fmt.Errorf("missing @parameter tag for parameter: %s", bodyParam)
-			}
+			return nil, fmt.Errorf("missing definitions for parameters %v found in the policy `%s`", paramsDiff, file.Name)
 		}
 
 		rego := Rego{
@@ -569,8 +661,10 @@ func parseDirectory(directory string, parseImports bool) ([]Rego, error) {
 			rules:          rules,
 			parameters:     headerParams,
 			headerComments: headerComments,
-			raw:            trimEachLine(string(file.Raw)),
+			raw:            string(file.Raw),
+			sanitizedRaw:   sanitizeRawSource(file.Raw),
 			skipConstraint: hasSkipConstraintTag(headerComments),
+			enforcement:    getEnforcementTag(headerComments),
 			annotations:    annotations,
 		}
 
@@ -592,6 +686,14 @@ func parseDirectory(directory string, parseImports bool) ([]Rego, error) {
 	return regos, nil
 }
 
+func sanitizeRawSource(raw []byte) string {
+	// Many YAML parsers have problems handling carriage returns and tabs so we sanitize the Rego
+	// before storing it so it can be rendered properly.
+	raw = bytes.ReplaceAll(raw, []byte("\r"), []byte(""))
+	raw = bytes.ReplaceAll(raw, []byte("\t"), []byte("  "))
+	return trimEachLine(string(raw))
+}
+
 func getRuleParamNames(rules []*ast.Rule) []string {
 	re := regexp.MustCompile(`input\.parameters\.([a-zA-Z0-9_-]+)`)
 	var ruleParams []string
@@ -608,7 +710,7 @@ func getRuleParamNames(rules []*ast.Rule) []string {
 }
 
 func getHeaderParams(annotations *ast.Annotations) []Parameter {
-	params, ok := annotations.Custom["parameters"].(map[string]any)
+	params, ok := annotations.Custom[annoParameters].(map[string]any)
 	if !ok {
 		return nil
 	}


Test patch:
diff --git a/examples/container-deny-latest-tag/src.rego b/examples/container-deny-latest-tag/src.rego
index e2fd904..d8be754 100644
--- a/examples/container-deny-latest-tag/src.rego
+++ b/examples/container-deny-latest-tag/src.rego
@@ -1,24 +1,36 @@
-# @title Images must not use the latest tag
+# METADATA
+# title: Images must not use the latest tag
+# description: |-
+#   Using the latest tag on images can cause unexpected problems in production. By specifying a pinned version
+#   we can have higher confidence that our applications are immutable and do not change unexpectedly.
 #
-# Using the latest tag on images can cause unexpected problems in production. By specifying a pinned version
-# we can have higher confidence that our applications are immutable and do not change unexpectedly.
+#   The following snippet is an example of how to satisfy this requirement:
 #
-# The following snippet is an example of how to satisfy this requirement:
-#
-# ```
-# apiVersion: apps/v1
-# kind: Deployment
-# metadata:
-#   name: redis
-# spec:
-#   template:
-#     spec:
-#       containers:
-#         - name: redis
-#           image: redis:6.2
-# ```
-#
-# @kinds apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+#    ```
+#    apiVersion: apps/v1
+#    kind: Deployment
+#    metadata:
+#      name: redis
+#    spec:
+#      template:
+#        spec:
+#          containers:
+#            - name: redis
+#              image: redis:6.2
+#   ```
+# custom:
+#   matchers:
+#     kinds:
+#     - apiGroups:
+#       - ""
+#       kinds:
+#       - Pod
+#     - apiGroups:
+#       - apps
+#       kinds:
+#       - DaemonSet
+#       - Deployment
+#       - StatefulSet
 package container_deny_latest_tag
 
 import data.lib.core
diff --git a/internal/commands/create_test.go b/internal/commands/create_test.go
deleted file mode 100644
index 55da37f..0000000
--- a/internal/commands/create_test.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package commands
-
-import (
-	"reflect"
-	"testing"
-)
-
-func TestToInterfaceSlice(t *testing.T) {
-	type args struct {
-		input []string
-	}
-	tests := []struct {
-		name string
-		args args
-		want []interface{}
-	}{
-		{
-			"empty",
-			args{[]string{}},
-			[]interface{}{},
-		},
-		{
-			"valid",
-			args{[]string{"a", "b"}},
-			[]interface{}{"a", "b"},
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if got := toInterfaceSlice(tt.args.input); !reflect.DeepEqual(got, tt.want) {
-				t.Errorf("toInterfaceSlice() = %v, want %v", got, tt.want)
-			}
-		})
-	}
-}
diff --git a/internal/rego/rego_test.go b/internal/rego/rego_test.go
index 734cb44..5e81532 100644
--- a/internal/rego/rego_test.go
+++ b/internal/rego/rego_test.go
@@ -96,7 +96,7 @@ third
 `
 
 	rego := Rego{
-		raw: raw,
+		sanitizedRaw: raw,
 	}
 
 	actual := rego.Source()
@@ -116,11 +116,8 @@ func TestEnforcement(t *testing.T) {
 		"@enforcement dryrun",
 		"@kinds apps/Deployment",
 	}
-	rego := Rego{
-		headerComments: comments,
-	}
 
-	actual := rego.Enforcement()
+	actual := getEnforcementTag(comments)
 	const expected = "dryrun"
 	if actual != expected {
 		t.Errorf("unexpected Enforcement. expected %v, actual %v", expected, actual)