Repository: plexsystems/konstraint
Pull request #69 (2020-08-31 17:48:09)

Consolidate Gatekeeper and Rego packages
This spawned from reading through our current docs and shoring up anything that was out of date. Working through a couple of inconsistencies, it just felt better to try to continue to make more sense out of our packages.

The goal was more or less to have a no-op when it comes to changes in outputs (i.e. generated policies, docs) and focus more on the refactoring effort to make review easier. 

The only notable set of changes comes from adding the hyphen to each policy which results from removing the newline when printing the source (_see [chomping](https://yaml.org/spec/1.2/spec.html#id2794534)_). This makes the output less opinionated.

This should provide a more consistent experience when parsing data from each of the policies. Each command can then take the extracted data, and manipulate/use it in a way that makes sense for the command.

### Freebies
- Library folder no longer matters. The rego files are loaded and the dependencies are built out based on the name of the package(s) that the rego file imports.

- The loaded rego files are now be _compiled_ during the `create` step. This makes sure that the entire set of rego policies are actually valid before running `create`. Previously, no validation was performed.

- Since all rego is pre-loaded, this allows for package consumers to use the methods on the Rego type without error checking. e.g. `rego.Libraries()`

Overall this should give more cohesion to the rego package and keep the CLI concerns separate from the package. e.g. The create command no longer needs to figure out the library directory, or call GetKinds/GetName--It's all a part of the loaded rego type.

**NOTE**
Since the docs/policies/etc are left _mostly_ unchanged and this is more of a refactoring effort--it may be better to view the structure/code on GitHub rather than trying to make sense of the diffs.


Patch:
diff --git a/acceptance.bats b/acceptance.bats
index c47a590..8c17faa 100644
--- a/acceptance.bats
+++ b/acceptance.bats
@@ -5,19 +5,12 @@
   git diff --quiet -- examples/policies.md
 }
 
-@test "[DOC] Outputting documentation to a different output directory" {
-  run ./build/konstraint doc examples --output test/doc/expected.md
-  git diff --quiet -- test/doc/expected.md
-}
-
 @test "[CREATE] Creating constraints and templates matches examples" {
   run ./build/konstraint create examples
-  git diff --quiet -- examples/container-images/constraint.yaml
-  git diff --quiet -- examples/container-images/template.yaml
+  git diff --quiet -- examples/
 }
 
 @test "[CREATE] Creating constraints using --output matches expected output" {
-  run ./build/konstraint create test/create --output test/create
-  git diff --quiet -- test/create/constraint_Create.yaml
-  git diff --quiet -- test/create/template_Create.yaml
+  run ./build/konstraint create test --output test
+  git diff --quiet -- test/
 }
diff --git a/docs/constraint_creation.md b/docs/constraint_creation.md
index 0601048..50a904e 100644
--- a/docs/constraint_creation.md
+++ b/docs/constraint_creation.md
@@ -2,17 +2,24 @@
 
 ## Policy File Parsing
 
-Konstraint only creates constraints for policies with `violation[]` rules in them, as that is required by Gatekeeper. If a policy does not have a violation rule in it, it is skipped.
+When using `konstraint generate`, Konstraint will only generate templates and constraints for policy files with at least one `violation[]` rule. The `violation` rule is required by Gatekeeper when evaluating policies on a Kubernetes cluster.
 
-## Importing Libraries
+When using `konstraint doc`, Konstraint will create documentation for each policy file and assign a severity based on the rule names found in the policy.
+
+The following rule names are organized into their own sections as they have special meaning within the context of Gatekeeper and Conftest:
 
-Libraries can be stored in a folder named `lib` or the name of the folder set with the `--lib` flag.
+- `violation` (supported by Gatekeeper and Conftest)
+- `warn` (supported by Conftest)
 
-Libraries will be added to the `ConstraintTemplate` if and only if the associated policy imports the library. This helps prevent importing Rego code that will go unused.
+If a policy file does not contain any of the above rules, the policy is added to the `Other` section.
+
+## Importing Libraries
+
+The Rego for the libraries will be added to the generated `ConstraintTemplate` if and only if the policy imports the library. This helps prevent importing Rego code that will go unused.
 
 ## Resource Naming
 
-The name of the ConstraintTemplate and Constraint are derived from the name of the folder that the policy was found in.
+The name of the templates and constraints are derived from the name of the folder that the policy was found in.
 
 For example, a policy found in: `policies/pod-volume-size-limits/src.rego` generates the following in the `policies/pod-volume-size-limits` directory:
 
@@ -25,20 +32,43 @@ For example, a policy found in: `policies/pod-volume-size-limits/src.rego` gener
   - kind: _PodVolumeSizeLimits_
   - name: _podvolumesizelimits_
 
-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/tree/master/library).
+When using the `--output` parameter, 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/tree/master/library)._
 
 ## Annotating Rules
 
-**Note: This feature is experimental.**
+To further promote that the `.rego` file is the source of truth for policy, a block comment can be added to each policy file.
 
-To further promote that the `.rego` file is the source of truth for policy, a form of block comments on each `violation` rule can be added that includes a human readable description of what the policy does. This comment block is used to set the matchers on the `Constraint` itself and to generate the policy documentation with `konstraint doc`.
+This comment block should:
+
+- Include a human readable description of what the policy does.
+- Set the matchers used when generating the Constraints.
 
 ```rego
-# All images deployed to the cluster must not contain a latest tag.
-# @Kinds apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+# @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
+package pod_deny_host_ipc
+
+import data.lib.core
+import data.lib.pods
+
 violation[msg] {
-  has_latest_tag
+    pod_has_hostipc
 
-  msg := k8s.format(sprintf("(%s) %s: Images must not use the latest tag", [k8s.kind, k8s.name]))
+    msg := core.format(sprintf("%s/%s: Pod allows for accessing the host IPC", [core.kind, core.name]))
+}
+
+pod_has_hostipc {
+    pods.pod.spec.hostIPC
 }
 ```
+
+The comment block is also what is used when generating documentation via the `doc` command.
diff --git a/examples/container-deny-added-caps/template.yaml b/examples/container-deny-added-caps/template.yaml
index 5a87689..b406a4b 100755
--- a/examples/container-deny-added-caps/template.yaml
+++ b/examples/container-deny-added-caps/template.yaml
@@ -10,7 +10,7 @@ spec:
         kind: ContainerDenyAddedCaps
   targets:
   - libs:
-    - |
+    - |-
       package lib.core
 
       default is_gatekeeper = false
@@ -53,7 +53,7 @@ spec:
       missing_field(obj, field) = true {
           not has_field(obj, field)
       }
-    - |
+    - |-
       package lib.pods
 
       import data.lib.core
@@ -82,7 +82,7 @@ spec:
       volumes[volume] {
           volume = pod.spec.volumes[_]
       }
-    - |
+    - |-
       package lib.security
 
       dropped_capability(container, cap) {
@@ -104,7 +104,7 @@ spec:
       added_capability(psp, cap) {
           lower(psp.spec.defaultAddCapabilities[_]) == lower(cap)
       }
-    rego: |
+    rego: |-
       package container_deny_added_caps
 
       import data.lib.core
diff --git a/examples/container-deny-escalation/template.yaml b/examples/container-deny-escalation/template.yaml
index 6902791..336d743 100755
--- a/examples/container-deny-escalation/template.yaml
+++ b/examples/container-deny-escalation/template.yaml
@@ -10,7 +10,7 @@ spec:
         kind: ContainerDenyEscalation
   targets:
   - libs:
-    - |
+    - |-
       package lib.core
 
       default is_gatekeeper = false
@@ -53,7 +53,7 @@ spec:
       missing_field(obj, field) = true {
           not has_field(obj, field)
       }
-    - |
+    - |-
       package lib.pods
 
       import data.lib.core
@@ -82,7 +82,7 @@ spec:
       volumes[volume] {
           volume = pod.spec.volumes[_]
       }
-    rego: |
+    rego: |-
       package container_deny_escalation
 
       import data.lib.core
diff --git a/examples/container-deny-privileged/template.yaml b/examples/container-deny-privileged/template.yaml
index c7d3dba..25fc27f 100755
--- a/examples/container-deny-privileged/template.yaml
+++ b/examples/container-deny-privileged/template.yaml
@@ -10,7 +10,7 @@ spec:
         kind: ContainerDenyPrivileged
   targets:
   - libs:
-    - |
+    - |-
       package lib.core
 
       default is_gatekeeper = false
@@ -53,7 +53,7 @@ spec:
       missing_field(obj, field) = true {
           not has_field(obj, field)
       }
-    - |
+    - |-
       package lib.pods
 
       import data.lib.core
@@ -82,7 +82,7 @@ spec:
       volumes[volume] {
           volume = pod.spec.volumes[_]
       }
-    - |
+    - |-
       package lib.security
 
       dropped_capability(container, cap) {
@@ -104,7 +104,7 @@ spec:
       added_capability(psp, cap) {
           lower(psp.spec.defaultAddCapabilities[_]) == lower(cap)
       }
-    rego: |
+    rego: |-
       package container_deny_privileged
 
       import data.lib.core
diff --git a/examples/container-deny-without-resource-constraints/template.yaml b/examples/container-deny-without-resource-constraints/template.yaml
index f3d1be3..12cb9c7 100644
--- a/examples/container-deny-without-resource-constraints/template.yaml
+++ b/examples/container-deny-without-resource-constraints/template.yaml
@@ -10,7 +10,7 @@ spec:
         kind: ContainerDenyWithoutResourceConstraints
   targets:
   - libs:
-    - |
+    - |-
       package lib.core
 
       default is_gatekeeper = false
@@ -53,7 +53,7 @@ spec:
       missing_field(obj, field) = true {
           not has_field(obj, field)
       }
-    - |
+    - |-
       package lib.pods
 
       import data.lib.core
@@ -82,7 +82,7 @@ spec:
       volumes[volume] {
           volume = pod.spec.volumes[_]
       }
-    rego: |
+    rego: |-
       package container_deny_without_resource_constraints
 
       import data.lib.core
diff --git a/examples/pod-deny-host-alias/template.yaml b/examples/pod-deny-host-alias/template.yaml
index 53fce2c..346d6c0 100755
--- a/examples/pod-deny-host-alias/template.yaml
+++ b/examples/pod-deny-host-alias/template.yaml
@@ -10,7 +10,7 @@ spec:
         kind: PodDenyHostAlias
   targets:
   - libs:
-    - |
+    - |-
       package lib.core
 
       default is_gatekeeper = false
@@ -53,7 +53,7 @@ spec:
       missing_field(obj, field) = true {
           not has_field(obj, field)
       }
-    - |
+    - |-
       package lib.pods
 
       import data.lib.core
@@ -82,7 +82,7 @@ spec:
       volumes[volume] {
           volume = pod.spec.volumes[_]
       }
-    rego: |
+    rego: |-
       package pod_deny_host_alias
 
       import data.lib.core
diff --git a/examples/pod-deny-host-ipc/template.yaml b/examples/pod-deny-host-ipc/template.yaml
index 9c7e6fe..8496481 100755
--- a/examples/pod-deny-host-ipc/template.yaml
+++ b/examples/pod-deny-host-ipc/template.yaml
@@ -10,7 +10,7 @@ spec:
         kind: PodDenyHostIpc
   targets:
   - libs:
-    - |
+    - |-
       package lib.core
 
       default is_gatekeeper = false
@@ -53,7 +53,7 @@ spec:
       missing_field(obj, field) = true {
           not has_field(obj, field)
       }
-    - |
+    - |-
       package lib.pods
 
       import data.lib.core
@@ -82,7 +82,7 @@ spec:
       volumes[volume] {
           volume = pod.spec.volumes[_]
       }
-    rego: |
+    rego: |-
       package pod_deny_host_ipc
 
       import data.lib.core
diff --git a/examples/pod-deny-host-network/template.yaml b/examples/pod-deny-host-network/template.yaml
index dd8c3c0..169d293 100755
--- a/examples/pod-deny-host-network/template.yaml
+++ b/examples/pod-deny-host-network/template.yaml
@@ -10,7 +10,7 @@ spec:
         kind: PodDenyHostNetwork
   targets:
   - libs:
-    - |
+    - |-
       package lib.core
 
       default is_gatekeeper = false
@@ -53,7 +53,7 @@ spec:
       missing_field(obj, field) = true {
           not has_field(obj, field)
       }
-    - |
+    - |-
       package lib.pods
 
       import data.lib.core
@@ -82,7 +82,7 @@ spec:
       volumes[volume] {
           volume = pod.spec.volumes[_]
       }
-    rego: |
+    rego: |-
       package pod_deny_host_network
 
       import data.lib.core
diff --git a/examples/pod-deny-host-pid/template.yaml b/examples/pod-deny-host-pid/template.yaml
index dc29c3e..b297b14 100755
--- a/examples/pod-deny-host-pid/template.yaml
+++ b/examples/pod-deny-host-pid/template.yaml
@@ -10,7 +10,7 @@ spec:
         kind: PodDenyHostPid
   targets:
   - libs:
-    - |
+    - |-
       package lib.core
 
       default is_gatekeeper = false
@@ -53,7 +53,7 @@ spec:
       missing_field(obj, field) = true {
           not has_field(obj, field)
       }
-    - |
+    - |-
       package lib.pods
 
       import data.lib.core
@@ -82,7 +82,7 @@ spec:
       volumes[volume] {
           volume = pod.spec.volumes[_]
       }
-    rego: |
+    rego: |-
       package pod_deny_host_pid
 
       import data.lib.core
diff --git a/examples/pod-deny-without-runasnonroot/template.yaml b/examples/pod-deny-without-runasnonroot/template.yaml
index 6854830..1090b9a 100755
--- a/examples/pod-deny-without-runasnonroot/template.yaml
+++ b/examples/pod-deny-without-runasnonroot/template.yaml
@@ -10,7 +10,7 @@ spec:
         kind: PodDenyWithoutRunasnonroot
   targets:
   - libs:
-    - |
+    - |-
       package lib.pods
 
       import data.lib.core
@@ -39,7 +39,7 @@ spec:
       volumes[volume] {
           volume = pod.spec.volumes[_]
       }
-    - |
+    - |-
       package lib.core
 
       default is_gatekeeper = false
@@ -82,7 +82,7 @@ spec:
       missing_field(obj, field) = true {
           not has_field(obj, field)
       }
-    rego: |
+    rego: |-
       package pod_deny_without_runasnonroot
 
       import data.lib.pods
diff --git a/examples/psp-deny-added-caps/template.yaml b/examples/psp-deny-added-caps/template.yaml
index e8b773c..d254ee7 100755
--- a/examples/psp-deny-added-caps/template.yaml
+++ b/examples/psp-deny-added-caps/template.yaml
@@ -10,7 +10,7 @@ spec:
         kind: PspDenyAddedCaps
   targets:
   - libs:
-    - |
+    - |-
       package lib.core
 
       default is_gatekeeper = false
@@ -53,7 +53,7 @@ spec:
       missing_field(obj, field) = true {
           not has_field(obj, field)
       }
-    - |
+    - |-
       package lib.psps
 
       import data.lib.core
@@ -76,7 +76,7 @@ spec:
           not is_exception
           psp = core.resource
       }
-    - |
+    - |-
       package lib.security
 
       dropped_capability(container, cap) {
@@ -98,7 +98,7 @@ spec:
       added_capability(psp, cap) {
           lower(psp.spec.defaultAddCapabilities[_]) == lower(cap)
       }
-    rego: |
+    rego: |-
       package psp_deny_added_caps
 
       import data.lib.core
diff --git a/examples/psp-deny-escalation/template.yaml b/examples/psp-deny-escalation/template.yaml
index 42ba5df..073222f 100755
--- a/examples/psp-deny-escalation/template.yaml
+++ b/examples/psp-deny-escalation/template.yaml
@@ -10,7 +10,7 @@ spec:
         kind: PspDenyEscalation
   targets:
   - libs:
-    - |
+    - |-
       package lib.core
 
       default is_gatekeeper = false
@@ -53,7 +53,7 @@ spec:
       missing_field(obj, field) = true {
           not has_field(obj, field)
       }
-    - |
+    - |-
       package lib.psps
 
       import data.lib.core
@@ -76,7 +76,7 @@ spec:
           not is_exception
           psp = core.resource
       }
-    rego: |
+    rego: |-
       package psp_deny_escalation
 
       import data.lib.core
diff --git a/examples/psp-deny-host-alias/template.yaml b/examples/psp-deny-host-alias/template.yaml
index 531da2e..3638be2 100755
--- a/examples/psp-deny-host-alias/template.yaml
+++ b/examples/psp-deny-host-alias/template.yaml
@@ -10,7 +10,7 @@ spec:
         kind: PspDenyHostAlias
   targets:
   - libs:
-    - |
+    - |-
       package lib.core
 
       default is_gatekeeper = false
@@ -53,7 +53,7 @@ spec:
       missing_field(obj, field) = true {
           not has_field(obj, field)
       }
-    - |
+    - |-
       package lib.psps
 
       import data.lib.core
@@ -76,7 +76,7 @@ spec:
           not is_exception
           psp = core.resource
       }
-    rego: |
+    rego: |-
       package psp_deny_host_alias
 
       import data.lib.core
diff --git a/examples/psp-deny-host-ipc/template.yaml b/examples/psp-deny-host-ipc/template.yaml
index ba7120a..95c056f 100755
--- a/examples/psp-deny-host-ipc/template.yaml
+++ b/examples/psp-deny-host-ipc/template.yaml
@@ -10,7 +10,7 @@ spec:
         kind: PspDenyHostIpc
   targets:
   - libs:
-    - |
+    - |-
       package lib.core
 
       default is_gatekeeper = false
@@ -53,7 +53,7 @@ spec:
       missing_field(obj, field) = true {
           not has_field(obj, field)
       }
-    - |
+    - |-
       package lib.psps
 
       import data.lib.core
@@ -76,7 +76,7 @@ spec:
           not is_exception
           psp = core.resource
       }
-    rego: |
+    rego: |-
       package psp_deny_host_ipc
 
       import data.lib.core
diff --git a/examples/psp-deny-host-network/template.yaml b/examples/psp-deny-host-network/template.yaml
index 81b4694..e9599d1 100755
--- a/examples/psp-deny-host-network/template.yaml
+++ b/examples/psp-deny-host-network/template.yaml
@@ -10,7 +10,7 @@ spec:
         kind: PspDenyHostNetwork
   targets:
   - libs:
-    - |
+    - |-
       package lib.core
 
       default is_gatekeeper = false
@@ -53,7 +53,7 @@ spec:
       missing_field(obj, field) = true {
           not has_field(obj, field)
       }
-    - |
+    - |-
       package lib.psps
 
       import data.lib.core
@@ -76,7 +76,7 @@ spec:
           not is_exception
           psp = core.resource
       }
-    rego: |
+    rego: |-
       package psp_deny_host_network
 
       import data.lib.core
diff --git a/examples/psp-deny-host-pid/template.yaml b/examples/psp-deny-host-pid/template.yaml
index 9481f05..0e06fbd 100755
--- a/examples/psp-deny-host-pid/template.yaml
+++ b/examples/psp-deny-host-pid/template.yaml
@@ -10,7 +10,7 @@ spec:
         kind: PspDenyHostPid
   targets:
   - libs:
-    - |
+    - |-
       package lib.core
 
       default is_gatekeeper = false
@@ -53,7 +53,7 @@ spec:
       missing_field(obj, field) = true {
           not has_field(obj, field)
       }
-    - |
+    - |-
       package lib.psps
 
       import data.lib.core
@@ -76,7 +76,7 @@ spec:
           not is_exception
           psp = core.resource
       }
-    rego: |
+    rego: |-
       package psp_deny_host_pid
 
       import data.lib.core
diff --git a/examples/psp-deny-privileged/template.yaml b/examples/psp-deny-privileged/template.yaml
index e4bffcb..9d168d2 100755
--- a/examples/psp-deny-privileged/template.yaml
+++ b/examples/psp-deny-privileged/template.yaml
@@ -10,7 +10,7 @@ spec:
         kind: PspDenyPrivileged
   targets:
   - libs:
-    - |
+    - |-
       package lib.core
 
       default is_gatekeeper = false
@@ -53,7 +53,7 @@ spec:
       missing_field(obj, field) = true {
           not has_field(obj, field)
       }
-    - |
+    - |-
       package lib.psps
 
       import data.lib.core
@@ -76,7 +76,7 @@ spec:
           not is_exception
           psp = core.resource
       }
-    rego: |
+    rego: |-
       package psp_deny_privileged
 
       import data.lib.core
diff --git a/examples/role-deny-use-privileged-psp/template.yaml b/examples/role-deny-use-privileged-psp/template.yaml
index ad1d761..a22a3ce 100755
--- a/examples/role-deny-use-privileged-psp/template.yaml
+++ b/examples/role-deny-use-privileged-psp/template.yaml
@@ -10,7 +10,7 @@ spec:
         kind: RoleDenyUsePrivilegedPsp
   targets:
   - libs:
-    - |
+    - |-
       package lib.core
 
       default is_gatekeeper = false
@@ -53,7 +53,7 @@ spec:
       missing_field(obj, field) = true {
           not has_field(obj, field)
       }
-    - |
+    - |-
       package lib.rbac
 
       import data.lib.core
@@ -75,7 +75,7 @@ spec:
       rule_has_resource_name(rule, name) {
           core.missing_field(rule, "resourceNames")
       }
-    - |
+    - |-
       package lib.security
 
       dropped_capability(container, cap) {
@@ -97,7 +97,7 @@ spec:
       added_capability(psp, cap) {
           lower(psp.spec.defaultAddCapabilities[_]) == lower(cap)
       }
-    rego: |
+    rego: |-
       package role_deny_use_privileged_psps
 
       import data.lib.core
diff --git a/internal/commands/create.go b/internal/commands/create.go
index 8c0d12f..37a5b3b 100644
--- a/internal/commands/create.go
+++ b/internal/commands/create.go
@@ -5,7 +5,6 @@ import (
 	"io/ioutil"
 	"os"
 	"path/filepath"
-	"strings"
 
 	"github.com/plexsystems/konstraint/internal/rego"
 
@@ -56,74 +55,32 @@ Create constraints with the Gatekeeper enforcement action set to dryrun
 }
 
 func runCreateCommand(path string) error {
-	policies, err := rego.GetFilesWithRule(path, "violation")
+	violations, err := rego.GetViolations(path)
 	if err != nil {
-		return fmt.Errorf("get policies: %w", err)
+		return fmt.Errorf("get violations: %w", err)
 	}
 
-	libraryPath, err := getLibraryPath(path)
-	if err != nil {
-		return fmt.Errorf("get library path: %w", err)
-	}
-
-	for _, policy := range policies {
-		if len(policy.ImportPackages) > 0 && libraryPath == "" {
-			return fmt.Errorf("policy %v imported libraries %v, but libraries were not found", policy.FilePath, policy.ImportPackages)
-		}
-	}
-
-	var libraries []rego.File
-	if libraryPath != "" {
-		libraries, err = rego.GetFiles(libraryPath)
-		if err != nil {
-			return fmt.Errorf("get libraries: %w", err)
-		}
-	}
-
-	var templateFileName, constraintFileName, outputDir string
-	outputFlag := viper.GetString("output")
-	if outputFlag == "" {
-		templateFileName = "template.yaml"
-		constraintFileName = "constraint.yaml"
-	} else {
-		outputDir = outputFlag
-	}
-
-	for _, policy := range policies {
-		policyDir := filepath.Dir(policy.FilePath)
-
-		if outputFlag == "" {
-			outputDir = policyDir
-		} else {
-			templateFileName = fmt.Sprintf("template_%s.yaml", GetKindFromPath(policy.FilePath))
-			constraintFileName = fmt.Sprintf("constraint_%s.yaml", GetKindFromPath(policy.FilePath))
+	for _, violation := range violations {
+		templateFileName := "template.yaml"
+		constraintFileName := "constraint.yaml"
+		outputDir := filepath.Dir(violation.Path())
+		if viper.GetString("output") != "" {
+			outputDir = viper.GetString("output")
+			templateFileName = fmt.Sprintf("template_%s.yaml", violation.Kind())
+			constraintFileName = fmt.Sprintf("constraint_%s.yaml", violation.Kind())
 		}
 
-		importedLibraries := getImportedLibraries(policy, libraries)
-		var librariesContents []string
-		for _, library := range importedLibraries {
-			librariesContents = append(librariesContents, getRegoWithoutComments(library.Contents))
-		}
-
-		if _, err := os.Stat(outputDir); os.IsNotExist(err) {
-			err := os.MkdirAll(outputDir, os.ModePerm)
-			if err != nil {
-				return fmt.Errorf("create output directory: %w", err)
-			}
-		}
-
-		constraintTemplate := getConstraintTemplate(policy, librariesContents)
+		constraintTemplate := getConstraintTemplate(violation)
 		constraintTemplateBytes, err := yaml.Marshal(&constraintTemplate)
 		if err != nil {
 			return fmt.Errorf("marshal constrainttemplate: %w", err)
 		}
 
-		err = ioutil.WriteFile(filepath.Join(outputDir, templateFileName), constraintTemplateBytes, os.ModePerm)
-		if err != nil {
+		if err := ioutil.WriteFile(filepath.Join(outputDir, templateFileName), constraintTemplateBytes, os.ModePerm); err != nil {
 			return fmt.Errorf("writing template: %w", err)
 		}
 
-		constraint, err := getConstraint(policy)
+		constraint, err := getConstraint(violation)
 		if err != nil {
 			return fmt.Errorf("get constraint: %w", err)
 		}
@@ -141,30 +98,28 @@ func runCreateCommand(path string) error {
 	return nil
 }
 
-func getConstraintTemplate(policy rego.File, libraries []string) v1beta1.ConstraintTemplate {
-	kind := GetKindFromPath(policy.FilePath)
-
+func getConstraintTemplate(violation rego.Rego) v1beta1.ConstraintTemplate {
 	constraintTemplate := v1beta1.ConstraintTemplate{
 		TypeMeta: metav1.TypeMeta{
 			APIVersion: "templates.gatekeeper.sh/v1beta1",
 			Kind:       "ConstraintTemplate",
 		},
 		ObjectMeta: metav1.ObjectMeta{
-			Name: strings.ToLower(kind),
+			Name: violation.Name(),
 		},
 		Spec: v1beta1.ConstraintTemplateSpec{
 			CRD: v1beta1.CRD{
 				Spec: v1beta1.CRDSpec{
 					Names: v1beta1.Names{
-						Kind: kind,
+						Kind: violation.Kind(),
 					},
 				},
 			},
 			Targets: []v1beta1.Target{
 				{
 					Target: "admission.k8s.gatekeeper.sh",
-					Libs:   libraries,
-					Rego:   getRegoWithoutComments(policy.Contents),
+					Libs:   violation.Dependencies(),
+					Rego:   violation.Source(),
 				},
 			},
 		},
@@ -173,11 +128,17 @@ func getConstraintTemplate(policy rego.File, libraries []string) v1beta1.Constra
 	return constraintTemplate
 }
 
-func getConstraint(policy rego.File) (unstructured.Unstructured, error) {
-	kind := GetKindFromPath(policy.FilePath)
+func getConstraint(violation rego.Rego) (unstructured.Unstructured, error) {
 	constraint := unstructured.Unstructured{}
-	constraint.SetName(strings.ToLower(kind))
-	constraint.SetGroupVersionKind(schema.GroupVersionKind{Group: "constraints.gatekeeper.sh", Version: "v1beta1", Kind: kind})
+
+	gvk := schema.GroupVersionKind{
+		Group:   "constraints.gatekeeper.sh",
+		Version: "v1beta1",
+		Kind:    violation.Kind(),
+	}
+
+	constraint.SetGroupVersionKind(gvk)
+	constraint.SetName(violation.Name())
 
 	dryrun := viper.GetBool("dryrun")
 	if dryrun {
@@ -186,7 +147,7 @@ func getConstraint(policy rego.File) (unstructured.Unstructured, error) {
 		}
 	}
 
-	matchers := GetMatchersFromComments(policy.Comments)
+	matchers := violation.Matchers()
 	if len(matchers.KindMatchers) == 0 {
 		return constraint, nil
 	}
@@ -225,67 +186,3 @@ func getConstraint(policy rego.File) (unstructured.Unstructured, error) {
 
 	return constraint, nil
 }
-
-func getLibraryPath(path string) (string, error) {
-	libraryFolderNames := []string{"lib", "libs", "util", "utils"}
-
-	var libraryPath string
-	err := filepath.Walk(path, func(currentFilePath string, fileInfo os.FileInfo, err error) error {
-		if err != nil {
-			return fmt.Errorf("walk path: %w", err)
-		}
-
-		if fileInfo.IsDir() && fileInfo.Name() == ".git" {
-			return filepath.SkipDir
-		}
-
-		if fileInfo.IsDir() && contains(libraryFolderNames, fileInfo.Name()) {
-			libraryPath = currentFilePath
-			return nil
-		}
-
-		return nil
-	})
-	if err != nil {
-		return "", err
-	}
-
-	return libraryPath, nil
-}
-
-func getImportedLibraries(file rego.File, allLibraries []rego.File) []rego.File {
-	var importedLibraries []rego.File
-	for _, importedPackage := range file.ImportPackages {
-		for _, library := range allLibraries {
-			if library.PackageName == importedPackage {
-				importedLibraries = append(importedLibraries, library)
-			}
-		}
-	}
-
-	for _, library := range importedLibraries {
-		importedLibraries = append(importedLibraries, getImportedLibraries(library, allLibraries)...)
-	}
-
-	importedLibraries = getUniqueRegoFiles(importedLibraries)
-	return importedLibraries
-}
-
-func getUniqueRegoFiles(files []rego.File) []rego.File {
-	var uniqueFiles []rego.File
-	for _, file := range files {
-		var seen bool
-		for _, uniqueFile := range uniqueFiles {
-			if file.PackageName == uniqueFile.PackageName {
-				seen = true
-				break
-			}
-		}
-
-		if !seen {
-			uniqueFiles = append(uniqueFiles, file)
-		}
-	}
-
-	return uniqueFiles
-}
diff --git a/internal/commands/document.go b/internal/commands/document.go
index 98a71ad..f7a5209 100644
--- a/internal/commands/document.go
+++ b/internal/commands/document.go
@@ -89,28 +89,23 @@ func runDocCommand(path string) error {
 	return nil
 }
 
-func getDocumentation(path string, outputDirectory string) (map[string][]Document, error) {
-	policies, err := rego.GetFiles(path)
+func getDocumentation(path string, outputDirectory string) (map[rego.Severity][]Document, error) {
+	policies, err := rego.GetAllSeverities(path)
 	if err != nil {
-		return nil, fmt.Errorf("get files: %w", err)
+		return nil, fmt.Errorf("get all severities: %w", err)
 	}
 
-	documents := make(map[string][]Document)
+	documents := make(map[rego.Severity][]Document)
 	for _, policy := range policies {
-		header, err := getHeader(policy.Comments)
-		if err != nil {
-			return nil, fmt.Errorf("get header: %w", err)
-		}
-
-		if header.Title == "" {
+		if policy.Title() == "" {
 			continue
 		}
 
 		var url string
 		if viper.GetString("url") != "" {
-			url = viper.GetString("url") + "/" + policy.FilePath
+			url = viper.GetString("url") + "/" + policy.Path()
 		} else {
-			relPath, err := filepath.Rel(outputDirectory, policy.FilePath)
+			relPath, err := filepath.Rel(outputDirectory, policy.Path())
 			if err != nil {
 				return nil, fmt.Errorf("rel path: %w", err)
 			}
@@ -121,91 +116,25 @@ func getDocumentation(path string, outputDirectory string) (map[string][]Documen
 			url = strings.ReplaceAll(relDir, string(os.PathSeparator), "/")
 		}
 
-		regoWithoutComments := getRegoWithoutComments(policy.Contents)
-		document := Document{
-			Header: header,
-			URL:    trimContent(url),
-			Rego:   trimContent(regoWithoutComments),
+		header := Header{
+			Title:       policy.Title(),
+			Description: policy.Description(),
+			Resources:   policy.Matchers().String(),
+			Anchor:      strings.ToLower(strings.ReplaceAll(policy.Title(), " ", "-")),
 		}
 
-		if contains(policy.RuleNames, "violation") {
-			documents["Violation"] = append(documents["Violation"], document)
-			continue
+		document := Document{
+			Header: header,
+			URL:    url,
+			Rego:   policy.Source(),
 		}
 
-		if contains(policy.RuleNames, "warn") {
-			documents["Warning"] = append(documents["Warning"], document)
-			continue
+		if policy.Severity() == "" {
+			documents["Other"] = append(documents["Other"], document)
+		} else {
+			documents[policy.Severity()] = append(documents[policy.Severity()], document)
 		}
-
-		documents["Other"] = append(documents["Other"], document)
 	}
 
 	return documents, nil
 }
-
-func getHeader(comments []string) (Header, error) {
-	var title string
-	var description string
-	var resources string
-	for _, comment := range comments {
-		if strings.Contains(comment, "@title") {
-			title = strings.SplitAfter(comment, "@title")[1]
-			title = strings.TrimPrefix(title, " ")
-			continue
-		}
-
-		if strings.Contains(comment, "@kinds") {
-			matchers := GetMatchersFromComments([]string{comment})
-			for _, kindMatcher := range matchers.KindMatchers {
-				resources += kindMatcher.APIGroup + "/" + kindMatcher.Kind + " "
-			}
-			break
-		}
-
-		comment = strings.TrimSpace(comment)
-		description += comment + "\n"
-	}
-
-	anchor := strings.ReplaceAll(strings.ToLower(title), " ", "-")
-
-	header := Header{
-		Title:       trimContent(title),
-		Description: trimContent(description),
-		Resources:   trimContent(resources),
-		Anchor:      trimContent(anchor),
-	}
-
-	return header, nil
-}
-
-func trimContent(content string) string {
-	content = strings.Trim(content, " ")
-	content = strings.Trim(content, "\n")
-	return content
-}
-
-func getRegoWithoutComments(rego string) string {
-	var regoWithoutComments string
-	lines := strings.Split(rego, "\n")
-	for _, line := range lines {
-		if strings.HasPrefix(line, "#") {
-			continue
-		}
-
-		regoWithoutComments += line + "\n"
-	}
-
-	regoWithoutComments = strings.TrimSuffix(regoWithoutComments, "\n")
-	return regoWithoutComments
-}
-
-func contains(collection []string, item string) bool {
-	for _, value := range collection {
-		if strings.EqualFold(value, item) {
-			return true
-		}
-	}
-
-	return false
-}
diff --git a/internal/commands/gatekeeper.go b/internal/rego/matchers.go
similarity index 52%
rename from internal/commands/gatekeeper.go
rename to internal/rego/matchers.go
index 4414497..10263d4 100644
--- a/internal/commands/gatekeeper.go
+++ b/internal/rego/matchers.go
@@ -1,7 +1,6 @@
-package commands
+package rego
 
 import (
-	"path/filepath"
 	"strings"
 )
 
@@ -10,17 +9,27 @@ type Matchers struct {
 	KindMatchers []KindMatcher
 }
 
+func (m Matchers) String() string {
+	var result string
+	for _, kindMatcher := range m.KindMatchers {
+		result += kindMatcher.APIGroup + "/" + kindMatcher.Kind + " "
+	}
+	result = strings.TrimSpace(result)
+
+	return result
+}
+
 // KindMatcher are the matchers that are applied to constraints.
 type KindMatcher struct {
 	APIGroup string
 	Kind     string
 }
 
-// GetMatchersFromComments returns all of the matchers found in the collection of comments.
-func GetMatchersFromComments(comments []string) Matchers {
+// Matchers returns all of the matchers found in the rego file.
+func (r Rego) Matchers() Matchers {
 	var matchers Matchers
-	for _, comment := range comments {
-		if strings.Contains(comment, "@kinds") {
+	for _, comment := range r.comments {
+		if strings.HasPrefix(comment, "@kinds") {
 			matchers.KindMatchers = getKindMatchers(comment)
 		}
 	}
@@ -28,18 +37,12 @@ func GetMatchersFromComments(comments []string) Matchers {
 	return matchers
 }
 
-// GetKindFromPath returns the kind of the resource based on its file path.
-func GetKindFromPath(path string) string {
-	name := getNameFromPath(path)
-	kind := strings.ReplaceAll(name, " ", "")
-
-	return kind
-}
-
 func getKindMatchers(comment string) []KindMatcher {
 	var kindMatchers []KindMatcher
 
-	kindMatcherGroups := strings.Split(comment, " ")[2:]
+	kindMatcherText := strings.TrimSpace(strings.SplitAfter(comment, "@kinds")[1])
+	kindMatcherGroups := strings.Split(kindMatcherText, " ")
+
 	for _, kindMatcherGroup := range kindMatcherGroups {
 		kindMatcherSegments := strings.Split(kindMatcherGroup, "/")
 
@@ -53,11 +56,3 @@ func getKindMatchers(comment string) []KindMatcher {
 
 	return kindMatchers
 }
-
-func getNameFromPath(path string) string {
-	name := filepath.Base(filepath.Dir(path))
-	name = strings.ReplaceAll(name, "-", " ")
-	name = strings.Title(name)
-
-	return name
-}
diff --git a/internal/rego/rego.go b/internal/rego/rego.go
index 456842d..54832ce 100644
--- a/internal/rego/rego.go
+++ b/internal/rego/rego.go
@@ -2,196 +2,283 @@ package rego
 
 import (
 	"fmt"
-	"io/ioutil"
 	"os"
 	"path/filepath"
-	"regexp"
 	"sort"
 	"strings"
 
-	"github.com/open-policy-agent/opa/ast"
+	"github.com/open-policy-agent/opa/loader"
 )
 
-// File is a parsed Rego file.
-type File struct {
-	FilePath       string
-	PackageName    string
-	ImportPackages []string
-	Contents       string
-	RuleNames      []string
-	Comments       []string
+// Severity describes the severity level of the rego file.
+type Severity string
+
+// The defined severity levels represent the valid severity
+// levels that a rego file can have.
+const (
+	Violation Severity = "Violation"
+	Warning   Severity = "Warning"
+)
+
+// Rego represents a parsed rego file.
+type Rego struct {
+	path         string
+	raw          string
+	comments     []string
+	rules        []string
+	dependencies []string
 }
 
-// GetFiles gets all Rego files in the given path and its subdirectories.
-func GetFiles(path string) ([]File, error) {
-	filePaths, err := getFilePaths(path)
+// GetAllSeverities gets all of the rego files found in the given
+// directory as well as any subdirectories.
+// Only rego files that contain a valid severity will be returned.
+func GetAllSeverities(directory string) ([]Rego, error) {
+	regos, err := parseDirectory(directory)
 	if err != nil {
-		return nil, fmt.Errorf("load files: %w", err)
+		return nil, fmt.Errorf("parse directory: %w", err)
 	}
 
-	files, err := getFiles(filePaths)
-	if err != nil {
-		return nil, fmt.Errorf("load files: %w", err)
+	var allSeverities []Rego
+	for _, rego := range regos {
+		if rego.Severity() == "" {
+			continue
+		}
+
+		allSeverities = append(allSeverities, rego)
 	}
 
-	return files, nil
+	return allSeverities, nil
 }
 
-// GetFilesWithRule gets all Rego files in the given path and its subdirectories that contain the specified rule.
-func GetFilesWithRule(path string, rule string) ([]File, error) {
-	allFiles, err := GetFiles(path)
+// GetViolations gets all of the files found in the given
+// directory as well as any subdirectories.
+// Only rego files that have a severity of violation will be returned.
+func GetViolations(directory string) ([]Rego, error) {
+	regos, err := parseDirectory(directory)
 	if err != nil {
-		return nil, fmt.Errorf("load policies: %w", err)
+		return nil, fmt.Errorf("parse directory: %w", err)
 	}
 
-	filesWithRule := getFilesWithRule(allFiles, rule)
-	return filesWithRule, nil
-}
+	var violations []Rego
+	for _, rego := range regos {
+		if rego.Severity() != Violation {
+			continue
+		}
 
-// NewFile parses the rego and creates a File
-func NewFile(filePath string, contents string) (File, error) {
-	module, err := ast.ParseModule(filePath, contents)
-	if err != nil {
-		return File{}, fmt.Errorf("parse module: %w", err)
+		violations = append(violations, rego)
 	}
 
-	var importPackages []string
-	for i := range module.Imports {
-		importPackages = append(importPackages, module.Imports[i].Path.String())
-	}
+	return violations, nil
+}
 
-	ruleNames, err := getRuleNamesFromModule(module)
-	if err != nil {
-		return File{}, fmt.Errorf("get module rules: %w", err)
-	}
+// Path returns the original path of the rego file.
+func (r Rego) Path() string {
+	return r.path
+}
+
+// Severity returns the severity of the rego file.
+// When a rego file has multiple rules that are considered
+// to be different severities, the first rule is chosen.
+func (r Rego) Severity() Severity {
+	var severity Severity
+	for _, rule := range r.rules {
+		if rule == "violation" {
+			severity = Violation
+			break
+		}
 
-	var comments []string
-	for _, comment := range module.Comments {
-		comments = append(comments, string(comment.Text))
+		if rule == "warn" {
+			severity = Warning
+			break
+		}
 	}
 
-	file := File{
-		FilePath:       filePath,
-		PackageName:    module.Package.Path.String(),
-		ImportPackages: importPackages,
-		Contents:       contents,
-		RuleNames:      ruleNames,
-		Comments:       comments,
+	return severity
+}
+
+// Kind returns the Kubernetes Kind of the rego file.
+// The kind of the rego file is determined by the
+// name of the directory that the rego file exists in.
+func (r Rego) Kind() string {
+	kind := filepath.Base(filepath.Dir(r.Path()))
+	kind = strings.ReplaceAll(kind, "-", " ")
+	kind = strings.Title(kind)
+	kind = strings.ReplaceAll(kind, " ", "")
+
+	return kind
+}
+
+// Name returns the name of the rego file.
+// The name of the rego file is its kind as lowercase.
+func (r Rego) Name() string {
+	return strings.ToLower(r.Kind())
+}
+
+// Title returns the title found in the header comment of the rego file.
+func (r Rego) Title() string {
+	var title string
+	for _, comment := range r.comments {
+		if !strings.Contains(comment, "@title") {
+			continue
+		}
+
+		title = strings.SplitAfter(comment, "@title")[1]
+		break
 	}
 
-	return file, nil
+	return trimString(title)
 }
 
-func getFilePaths(path string) ([]string, error) {
-	var regoFilePaths []string
-	err := filepath.Walk(path, func(currentFilePath string, fileInfo os.FileInfo, err error) error {
-		if err != nil {
-			return fmt.Errorf("walk path: %w", err)
+// Description returns the entire description
+// found in the header comment of the rego file.
+func (r Rego) Description() string {
+	var description string
+	for _, comment := range r.comments {
+
+		// When the  token appears, we consider this point to
+		// be the end of the description.
+		if strings.HasPrefix(comment, "@kinds") {
+			break
 		}
 
-		if fileInfo.IsDir() && fileInfo.Name() == ".git" {
-			return filepath.SkipDir
+		if strings.HasPrefix(comment, "@") {
+			continue
 		}
 
-		if filepath.Ext(currentFilePath) != ".rego" || strings.HasSuffix(fileInfo.Name(), "_test.rego") {
-			return nil
+		description += comment
+		description += "\n"
+	}
+
+	return trimString(description)
+}
+
+// Source returns the original source code inside
+// of the rego file without any comments.
+func (r Rego) Source() string {
+	return removeComments(r.raw)
+}
+
+// Dependencies returns all of the source for the rego files that this
+// rego file depends on.
+func (r Rego) Dependencies() []string {
+	return r.dependencies
+}
+
+func parseDirectory(directory string) ([]Rego, error) {
+
+	// Recursively find all rego files (ignoring test files), starting at the given directory.
+	result, err := loader.NewFileLoader().Filtered([]string{directory}, func(abspath string, info os.FileInfo, depth int) bool {
+		if strings.HasSuffix(info.Name(), "_test.rego") {
+			return true
 		}
 
-		regoFilePaths = append(regoFilePaths, currentFilePath)
+		if !info.IsDir() && filepath.Ext(info.Name()) != ".rego" {
+			return true
+		}
 
-		return nil
+		return false
 	})
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("filter rego files: %w", err)
 	}
 
-	return regoFilePaths, nil
-}
+	if _, err := result.Compiler(); err != nil {
+		return nil, fmt.Errorf("compile: %w", err)
+	}
 
-func getFilesWithRule(regoFiles []File, rule string) []File {
-	var matchingPolicies []File
-	allPolicies := getPolicies(regoFiles)
-	for _, policy := range allPolicies {
-		for _, ruleName := range policy.RuleNames {
-			if ruleName == rule {
-				matchingPolicies = append(matchingPolicies, policy)
-			}
-		}
+	// 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 := make(map[string]*loader.RegoFile)
+	for _, regoFile := range result.Modules {
+		files[regoFile.Parsed.Package.Path.String()] = regoFile
 	}
 
-	return matchingPolicies
-}
+	var regos []Rego
+	for _, file := range files {
+		importPaths := getRecursiveImportPaths(file, files)
+		importPaths = dedupe(importPaths)
 
-func getPolicies(regoFiles []File) []File {
-	var policies []File
-	for _, regoFile := range regoFiles {
-		if len(regoFile.RuleNames) > 0 {
-			policies = append(policies, regoFile)
+		var dependencies []string
+		for _, importPath := range importPaths {
+			dependencies = append(dependencies, removeComments(string(files[importPath].Raw)))
 		}
-	}
 
-	return policies
-}
+		var rules []string
+		for r := range file.Parsed.Rules {
+			rules = append(rules, file.Parsed.Rules[r].Head.Name.String())
+		}
 
-func getFiles(files []string) ([]File, error) {
-	filesContents, err := readFilesContents(files)
-	if err != nil {
-		return nil, fmt.Errorf("read files: %w", err)
-	}
+		var comments []string
+		for c := range file.Parsed.Comments {
+			comments = append(comments, trimString(string(file.Parsed.Comments[c].Text)))
+		}
 
-	var regoFiles []File
-	for path, contents := range filesContents {
-		regoFile, err := NewFile(path, contents)
-		if err != nil {
-			return nil, fmt.Errorf("new rego file: %w", err)
+		// Many YAML parsers do not like rendering out CRLF when writing the YAML to disk.
+		// This causes ConstraintTemplates to be rendered with the line breaks as text,
+		// rather than the actual line break.
+		raw := strings.ReplaceAll(string(file.Raw), "\r", "")
+
+		rego := Rego{
+			path:         file.Name,
+			dependencies: dependencies,
+			rules:        rules,
+			comments:     comments,
+			raw:          raw,
 		}
 
-		regoFiles = append(regoFiles, regoFile)
+		regos = append(regos, rego)
 	}
 
-	sort.Slice(regoFiles, func(i, j int) bool {
-		return regoFiles[i].FilePath < regoFiles[j].FilePath
+	sort.Slice(regos, func(i, j int) bool {
+		return regos[i].path < regos[j].path
 	})
 
-	return regoFiles, nil
+	return regos, nil
 }
 
-func getRuleNamesFromModule(module *ast.Module) ([]string, error) {
-	re, err := regexp.Compile(`^\s*([a-z]+)\s*\[\s*\{?\s*"?msg`)
-	if err != nil {
-		return nil, fmt.Errorf("compile regex: %w", err)
-	}
-
-	var ruleNames []string
-	for _, rule := range module.Rules {
-		match := re.FindStringSubmatch(rule.Head.String())
-		if len(match) == 0 {
+func removeComments(raw string) string {
+	var regoWithoutComments string
+	lines := strings.Split(raw, "\n")
+	for _, line := range lines {
+		if strings.HasPrefix(line, "#") {
 			continue
 		}
-		if contains(ruleNames, match[1]) {
-			continue
-		}
-		ruleNames = append(ruleNames, match[1])
+
+		regoWithoutComments += line + "\n"
+	}
+
+	return trimString(regoWithoutComments)
+}
+
+func trimString(text string) string {
+	text = strings.TrimSpace(text)
+	text = strings.Trim(text, "\n")
+	return text
+}
+
+func getRecursiveImportPaths(regoFile *loader.RegoFile, regoFiles map[string]*loader.RegoFile) []string {
+	var recursiveImports []string
+	for i := range regoFile.Parsed.Imports {
+		imported := regoFiles[regoFile.Parsed.Imports[i].Path.String()]
+
+		recursiveImports = append(recursiveImports, imported.Parsed.Package.Path.String())
+		recursiveImports = append(recursiveImports, getRecursiveImportPaths(imported, regoFiles)...)
 	}
 
-	return ruleNames, nil
+	return recursiveImports
 }
 
-func readFilesContents(filePaths []string) (map[string]string, error) {
-	filesContents := make(map[string]string)
-	for _, filePath := range filePaths {
-		data, err := ioutil.ReadFile(filePath)
-		if err != nil {
-			return nil, fmt.Errorf("read file: %w", err)
+func dedupe(collection []string) []string {
+	var dedupedCollection []string
+	for _, item := range collection {
+		if contains(dedupedCollection, item) {
+			continue
 		}
 
-		// Many YAML parsers do not like rendering out CRLF when writing the YAML to disk.
-		// This causes ConstraintTemplates to be rendered with the line breaks as text,
-		// rather than the actual line break.
-		filesContents[filePath] = strings.ReplaceAll(string(data), "\r", "")
+		dedupedCollection = append(dedupedCollection, item)
 	}
 
-	return filesContents, nil
+	return dedupedCollection
 }
 
 func contains(collection []string, item string) bool {


Test patch:
diff --git a/examples/container-deny-latest-tag/template.yaml b/examples/container-deny-latest-tag/template.yaml
index de1cac6..33480a6 100644
--- a/examples/container-deny-latest-tag/template.yaml
+++ b/examples/container-deny-latest-tag/template.yaml
@@ -10,7 +10,7 @@ spec:
         kind: ContainerDenyLatestTag
   targets:
   - libs:
-    - |
+    - |-
       package lib.core
 
       default is_gatekeeper = false
@@ -53,7 +53,7 @@ spec:
       missing_field(obj, field) = true {
           not has_field(obj, field)
       }
-    - |
+    - |-
       package lib.pods
 
       import data.lib.core
@@ -82,7 +82,7 @@ spec:
       volumes[volume] {
           volume = pod.spec.volumes[_]
       }
-    rego: |
+    rego: |-
       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 83654f7..0000000
--- a/internal/commands/create_test.go
+++ /dev/null
@@ -1,198 +0,0 @@
-package commands
-
-import (
-	"fmt"
-	"testing"
-
-	"github.com/plexsystems/konstraint/internal/rego"
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
-)
-
-func TestGetConstraint_NoKinds_ReturnsEmptyMatcher(t *testing.T) {
-	policy := `package test
-# Description
-rule[msg] { msg = true }`
-
-	parsedPolicy, err := rego.NewFile("test.rego", policy)
-	if err != nil {
-		t.Fatal("new rego file:", err)
-	}
-
-	actual, err := getConstraint(parsedPolicy)
-	if err != nil {
-		t.Fatal("get constraint:", err)
-	}
-
-	if _, exists := actual.Object["spec"]; exists {
-		t.Errorf("expected no spec section, but one existed.")
-	}
-}
-
-func TestGetConstraint_KindsInComment_ReturnsKinds(t *testing.T) {
-	policy := `package test
-# Description
-# @kinds core/Pod apps/Deployment
-rule[msg] { msg = true }`
-
-	rego, err := rego.NewFile("test.rego", policy)
-	if err != nil {
-		t.Fatal("load policy rego file:", err)
-	}
-
-	actual, err := getConstraint(rego)
-	if err != nil {
-		t.Fatal("get constraint:", err)
-	}
-
-	kindMatchers, err := getConstraintKindMatchers(actual)
-	if err != nil {
-		t.Fatal("get constraint kind matchers:", err)
-	}
-
-	if len(kindMatchers) == 0 {
-		t.Errorf("expected kind matcher to exist, but none were found")
-	}
-
-	expectedGroups := []string{"", "apps"}
-	for _, expectedGroup := range expectedGroups {
-		if !contains(kindMatchers[0].apiGroups, expectedGroup) {
-			t.Errorf("expected apiGroup matcher to contain '%v', but was not found.", expectedGroup)
-		}
-	}
-
-	expectedKinds := []string{"Pod", "Deployment"}
-	for _, expectedKind := range expectedKinds {
-		if !contains(kindMatchers[0].kinds, expectedKind) {
-			t.Errorf("expected kind matcher to contain '%v', but was not found.", expectedKind)
-		}
-	}
-}
-
-func TestGetConstraintTemplate_CorrectLibrariesImported(t *testing.T) {
-	policyImportsFoo := `package test
-
-import data.lib.foo
-rule[msg] { msg = true }`
-
-	policyFile, err := rego.NewFile("/foo/test-kind/src.rego", policyImportsFoo)
-	if err != nil {
-		t.Fatal("new rego file:", err)
-	}
-
-	libraryRegos := []string{`package lib.foo`, `package lib.bar`}
-	var libraries []rego.File
-	for i, libraryRego := range libraryRegos {
-		lib, err := rego.NewFile(fmt.Sprintf("lib.%d.rego", i), libraryRego)
-		if err != nil {
-			t.Fatal("create library from rego")
-		}
-
-		libraries = append(libraries, lib)
-	}
-
-	var librariesContents []string
-	importedLibraries := getImportedLibraries(policyFile, libraries)
-	for _, library := range importedLibraries {
-		librariesContents = append(librariesContents, getRegoWithoutComments(library.Contents))
-	}
-
-	actual := getConstraintTemplate(policyFile, librariesContents)
-	if err != nil {
-		t.Fatal("get constraint:", err)
-	}
-
-	if len(actual.Spec.Targets) == 0 {
-		t.Errorf("expected target to exist, but none were found")
-	}
-
-	actualLibraryCount := len(actual.Spec.Targets[0].Libs)
-	if actualLibraryCount != 1 {
-		t.Errorf("expected 1 library to be added, but found %v", actualLibraryCount)
-	}
-}
-
-func TestRecursiveLibraryImport(t *testing.T) {
-	policyImportsLibA := `package test
-import data.lib.a`
-
-	libFiles := []struct {
-		path     string
-		contents string
-	}{
-		{path: "lib_a.rego", contents: "package lib.a\nimport data.lib.b"},
-		{path: "lib_b.rego", contents: "package lib.b"},
-	}
-
-	policy, err := rego.NewFile("test.rego", policyImportsLibA)
-	if err != nil {
-		t.Fatal("new policy file", err)
-	}
-
-	var libs []rego.File
-	for _, libFile := range libFiles {
-		lib, err := rego.NewFile(libFile.path, libFile.contents)
-		if err != nil {
-			t.Fatal("new library file", err)
-		}
-		libs = append(libs, lib)
-	}
-
-	importedLibraries := getImportedLibraries(policy, libs)
-	if len(importedLibraries) != 2 {
-		t.Error("recursive library import failed")
-	}
-}
-
-func TestGetKindFromPath(t *testing.T) {
-	path := "/path/to/rego/container-resource-limits/something.rego"
-
-	expected := "ContainerResourceLimits"
-	actual := GetKindFromPath(path)
-
-	if actual != expected {
-		t.Errorf("expected Kind of %v, but got %v", expected, actual)
-	}
-}
-
-// Helpers to extract the collection of
-// apiGroup and kind matchers in a Constraint
-type kindMatcher struct {
-	apiGroups []string
-	kinds     []string
-}
-
-func getConstraintKindMatchers(constraint unstructured.Unstructured) ([]kindMatcher, error) {
-	kindConfigs, exists, err := unstructured.NestedSlice(constraint.Object, "spec", "match", "kinds")
-	if !exists {
-		return nil, fmt.Errorf("kind config did not exist")
-	} else if err != nil {
-		return nil, fmt.Errorf("getting kind config: %w", err)
-	}
-
-	var kindMatchers []kindMatcher
-	for _, kindConfig := range kindConfigs {
-		currentKindConfig := kindConfig.(map[string]interface{})
-		apiGroups, exists, err := unstructured.NestedStringSlice(currentKindConfig, "apiGroups")
-		if !exists {
-			return nil, fmt.Errorf("apiGroups matcher did not exist")
-		} else if err != nil {
-			return nil, fmt.Errorf("getting apiGroups matchers: %w", err)
-		}
-
-		kinds, exists, err := unstructured.NestedStringSlice(currentKindConfig, "kinds")
-		if !exists {
-			return nil, fmt.Errorf("kinds matcher did not exist")
-		} else if err != nil {
-			return nil, fmt.Errorf("getting kinds matchers: %w", err)
-		}
-
-		kindMatcher := kindMatcher{
-			apiGroups: apiGroups,
-			kinds:     kinds,
-		}
-
-		kindMatchers = append(kindMatchers, kindMatcher)
-	}
-
-	return kindMatchers, nil
-}
diff --git a/internal/commands/document_test.go b/internal/commands/document_test.go
deleted file mode 100644
index bfbbe9a..0000000
--- a/internal/commands/document_test.go
+++ /dev/null
@@ -1,60 +0,0 @@
-package commands
-
-import (
-	"testing"
-
-	"github.com/plexsystems/konstraint/internal/rego"
-)
-
-func TestGetMatchersFromComments_NoKinds(t *testing.T) {
-	policy := `package test
-# Description
-violation[msg] {
-	false
-}`
-
-	regoFile, err := rego.NewFile("test.rego", policy)
-	if err != nil {
-		t.Fatal("new rego file", err)
-	}
-
-	actual := GetMatchersFromComments(regoFile.Comments)
-	if len(actual.KindMatchers) > 0 {
-		t.Error("expected no Kind matchers, but matchers were returned")
-	}
-}
-
-func TestGetMatchersFromComments_Kinds(t *testing.T) {
-	policy := `package test
-# First description
-# @kinds core/Pod apps/Deployment apps/DaemonSet
-violation[msg] {
-	false
-}`
-
-	regoFile, err := rego.NewFile("test.rego", policy)
-	if err != nil {
-		t.Fatal("new rego file", err)
-	}
-
-	actual := GetMatchersFromComments(regoFile.Comments)
-
-	expectedMatcherCount := 3
-	if len(actual.KindMatchers) != expectedMatcherCount {
-		t.Errorf("expected %v matchers to exist, but %v were found", expectedMatcherCount, len(actual.KindMatchers))
-	}
-
-	expectedGroups := []string{"core", "apps", "apps"}
-	for g, kindMatcher := range actual.KindMatchers {
-		if kindMatcher.APIGroup != expectedGroups[g] {
-			t.Errorf("expected group to be %v, but was %v", expectedGroups[g], kindMatcher.APIGroup)
-		}
-	}
-
-	expectedKinds := []string{"Pod", "Deployment", "DaemonSet"}
-	for k, kindMatcher := range actual.KindMatchers {
-		if kindMatcher.Kind != expectedKinds[k] {
-			t.Errorf("expected kind to be %v, but was %v", expectedKinds[k], kindMatcher.Kind)
-		}
-	}
-}
diff --git a/internal/rego/matchers_test.go b/internal/rego/matchers_test.go
new file mode 100644
index 0000000..2699b7f
--- /dev/null
+++ b/internal/rego/matchers_test.go
@@ -0,0 +1,26 @@
+package rego
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestGetKindMatchers(t *testing.T) {
+	comments := []string{
+		"@kinds core/Pod apps/Deployment",
+	}
+	rego := Rego{
+		comments: comments,
+	}
+
+	expected := []KindMatcher{
+		{APIGroup: "core", Kind: "Pod"},
+		{APIGroup: "apps", Kind: "Deployment"},
+	}
+
+	actual := rego.Matchers().KindMatchers
+
+	if !reflect.DeepEqual(expected, actual) {
+		t.Errorf("Unexpected KindMatchers. expected %v, actual %v.", expected, actual)
+	}
+}
diff --git a/internal/rego/rego_test.go b/internal/rego/rego_test.go
index c6173ac..54fa92c 100644
--- a/internal/rego/rego_test.go
+++ b/internal/rego/rego_test.go
@@ -1,69 +1,107 @@
 package rego
 
 import (
-	"reflect"
 	"testing"
 )
 
-var (
-	testRegoFiles = []File{
-		{
-			FilePath:  "missingViolation.rego",
-			Contents:  "package test\ndefault a = true",
-			RuleNames: nil,
-		},
-		{
-			FilePath:  "withViolation.rego",
-			Contents:  "package test\nviolation[msg] { msg = true }",
-			RuleNames: []string{"violation"},
-		},
-		{
-			FilePath:  "withWarn.rego",
-			Contents:  "package test\nwarn[msg] { msg = true }",
-			RuleNames: []string{"warn"},
-		},
+func TestKind(t *testing.T) {
+	policy := Rego{
+		path: "some/path/my-policy/src.rego",
+	}
+
+	actual := policy.Kind()
+
+	const expected = "MyPolicy"
+	if actual != expected {
+		t.Errorf("unexpected Kind. expected %v, actual %v", expected, actual)
+	}
+}
+
+func TestName(t *testing.T) {
+	policy := Rego{
+		path: "some/path/my-policy/src.rego",
+	}
+
+	actual := policy.Name()
+
+	const expected = "mypolicy"
+	if actual != expected {
+		t.Errorf("unexpected Name. expected %v, actual %v", expected, actual)
+	}
+}
+
+func TestTitle(t *testing.T) {
+	comments := []string{
+		"@title The title",
+	}
+
+	rego := Rego{
+		comments: comments,
 	}
-)
 
-func TestNewFile_RuleNames(t *testing.T) {
-	var rulesNamesTests = []struct {
-		policy    string
-		ruleCount int
-		ruleNames []string
-	}{
-		{"package test\ndefault test = true", 0, nil},
-		{"package test\nviolation[msg] { msg = true }", 1, []string{"violation"}},
-		{"package test\nwarn[msg] { msg = true }", 1, []string{"warn"}},
-		{"package test\nviolation[msg] { msg = true }\nwarn[msg] { msg = true }", 2, []string{"violation", "warn"}},
-		{"package test\nviolation[msg] { msg = true }\nviolation[msg] { msg = true }", 1, []string{"violation"}},
+	actual := rego.Title()
+
+	const expected = "The title"
+	if actual != expected {
+		t.Errorf("unexpected Title. expected %v, actual %v", expected, actual)
+	}
+}
+
+func TestDescription(t *testing.T) {
+	comments := []string{
+		"@title The title",
+		"The description",
+		"@kinds The kinds",
+		"Extra comment",
 	}
 
-	for _, test := range rulesNamesTests {
-		regoFile, err := NewFile("test.rego", test.policy)
-		if err != nil {
-			t.Fatal("newRegoFile")
-		}
+	rego := Rego{
+		comments: comments,
+	}
 
-		if len(regoFile.RuleNames) != test.ruleCount {
-			t.Errorf("expected rule names count to be %v, but was %v", test.ruleCount, len(regoFile.RuleNames))
-		}
+	actual := rego.Description()
 
-		if !reflect.DeepEqual(regoFile.RuleNames, test.ruleNames) {
-			t.Errorf("expected rule names to be %v, but was %v", test.ruleNames, regoFile.RuleNames)
-		}
+	const expected = "The description"
+	if actual != expected {
+		t.Errorf("unexpected Description. expected %v, actual %v", expected, actual)
 	}
 }
 
-func TestGetPolicies(t *testing.T) {
-	policies := getPolicies(testRegoFiles)
-	if len(policies) != 2 {
-		t.Errorf("expected %v policies, but got %v", 2, len(policies))
+func TestSeverity(t *testing.T) {
+	rules := []string{
+		"violation",
+		"warn",
+	}
+
+	rego := Rego{
+		rules: rules,
+	}
+
+	actual := rego.Severity()
+
+	const expected = Violation
+	if actual != expected {
+		t.Errorf("unexpected Severity. expected %v, actual %v", expected, actual)
 	}
 }
 
-func TestGetPoliciesWithRule(t *testing.T) {
-	matchingPolicies := getFilesWithRule(testRegoFiles, "warn")
-	if len(matchingPolicies) != 1 {
-		t.Errorf("expected %v policies, but got %v", 1, len(matchingPolicies))
+func TestSource(t *testing.T) {
+	raw := `first
+# second
+third
+# fourth
+`
+
+	rego := Rego{
+		raw: raw,
+	}
+
+	actual := rego.Source()
+
+	const expected = `first
+third`
+
+	if actual != expected {
+		t.Errorf("unexpected Source. expected %v, actual %v", expected, actual)
 	}
 }
diff --git a/test/create/constraint_Create.yaml b/test/constraint_Test.yaml
similarity index 87%
rename from test/create/constraint_Create.yaml
rename to test/constraint_Test.yaml
index 2ba133d..83e2933 100755
--- a/test/create/constraint_Create.yaml
+++ b/test/constraint_Test.yaml
@@ -1,7 +1,7 @@
 apiVersion: constraints.gatekeeper.sh/v1beta1
-kind: Create
+kind: Test
 metadata:
-  name: create
+  name: test
 spec:
   match:
     kinds:
diff --git a/test/create/lib/core.rego b/test/create/lib/core.rego
deleted file mode 100644
index 152d5e4..0000000
--- a/test/create/lib/core.rego
+++ /dev/null
@@ -1,5 +0,0 @@
-package lib.core
-
-format(msg) = {"msg": msg} {
-    true
-}
diff --git a/test/create/src.rego b/test/create/src.rego
deleted file mode 100644
index af0899c..0000000
--- a/test/create/src.rego
+++ /dev/null
@@ -1,13 +0,0 @@
-package create
-
-import data.lib.core
-
-# @title Images must not use the latest tag
-#
-# Using the latest tag on images can cause unexpected problems in production. By specifing a pinned version
-# we can have higher confidence that our applications are immutable and do not change unexpectedly.
-#
-# @kinds apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
-violation[msg] {
-    msg := core.format(sprintf("%s/%s: Images must not use the latest tag", [core.kind, core.name]))
-}
diff --git a/test/create/template_Create.yaml b/test/create/template_Create.yaml
deleted file mode 100755
index e9734b2..0000000
--- a/test/create/template_Create.yaml
+++ /dev/null
@@ -1,28 +0,0 @@
-apiVersion: templates.gatekeeper.sh/v1beta1
-kind: ConstraintTemplate
-metadata:
-  creationTimestamp: null
-  name: create
-spec:
-  crd:
-    spec:
-      names:
-        kind: Create
-  targets:
-  - libs:
-    - |
-      package lib.core
-
-      format(msg) = {"msg": msg} {
-          true
-      }
-    rego: |
-      package create
-
-      import data.lib.core
-
-      violation[msg] {
-          msg := core.format(sprintf("%s/%s: Images must not use the latest tag", [core.kind, core.name]))
-      }
-    target: admission.k8s.gatekeeper.sh
-status: {}
diff --git a/test/doc/expected.md b/test/doc/expected.md
deleted file mode 100644
index 841155a..0000000
--- a/test/doc/expected.md
+++ /dev/null
@@ -1,719 +0,0 @@
-# Policies
-
-## Violations
-
-* [Containers must drop all capabilities](#containers-must-drop-all-capabilities)
-* [Containers must not allow for privilege escalation](#containers-must-not-allow-for-privilege-escalation)
-* [Images must not use the latest tag](#images-must-not-use-the-latest-tag)
-* [Containers must not run as privileged](#containers-must-not-run-as-privileged)
-* [Containers must define resource constraints](#containers-must-define-resource-constraints)
-* [Pods must not have access to the host aliases](#pods-must-not-have-access-to-the-host-aliases)
-* [Pods must not run with access to the host IPC](#pods-must-not-run-with-access-to-the-host-ipc)
-* [Pods must not run with access to the host networking](#pods-must-not-run-with-access-to-the-host-networking)
-* [Pods must not run with access to the host PID namespace](#pods-must-not-run-with-access-to-the-host-pid-namespace)
-* [Pods must run as non-root](#pods-must-run-as-non-root)
-* [PodSecurityPolicies must require all capabilities are dropped](#podsecuritypolicies-must-require-all-capabilities-are-dropped)
-* [PodSecurityPolicies must not allow privileged escalation](#podsecuritypolicies-must-not-allow-privileged-escalation)
-* [PodSecurityPolicies must not allow access to the host aliases](#podsecuritypolicies-must-not-allow-access-to-the-host-aliases)
-* [PodSecurityPolicies must not allow access to the host IPC](#podsecuritypolicies-must-not-allow-access-to-the-host-ipc)
-* [PodSecurityPolicies must not allow access to the host network](#podsecuritypolicies-must-not-allow-access-to-the-host-network)
-* [PodSecurityPolicies must not allow access to the host PID namespace](#podsecuritypolicies-must-not-allow-access-to-the-host-pid-namespace)
-* [PodSecurityPolicies must require containers to not run as privileged](#podsecuritypolicies-must-require-containers-to-not-run-as-privileged)
-* [Roles must not allow use of privileged PodSecurityPolicies](#roles-must-not-allow-use-of-privileged-podsecuritypolicies)
-
-## Warnings
-
-* [Deprecated Deployment and DaemonSet API](#deprecated-deployment-and-daemonset-api)
-* [Containers should not have a writable root filesystem](#containers-should-not-have-a-writable-root-filesystem)
-* [PodSecurityPolicies should require that a read-only root filesystem is set](#podsecuritypolicies-should-require-that-a-read-only-root-filesystem-is-set)
-
-## Containers must drop all capabilities
-
-**Severity:** Violation
-
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
-
-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.
-
-### Rego
-
-```rego
-package container_deny_added_caps
-
-import data.lib.core
-import data.lib.pods
-import data.lib.security
-
-violation[msg] {
-    pods.containers[container]
-    not container_dropped_all_capabilities(container)
-
-    msg := core.format(sprintf("%s/%s/%s: Does not drop all capabilities", [core.kind, core.name, container.name]))
-}
-
-container_dropped_all_capabilities(container) {
-    security.dropped_capability(container, "all")
-}
-```
-
-_source: [../../examples/container-deny-added-caps](../../examples/container-deny-added-caps)_
-
-## Containers must not allow for privilege escalation
-
-**Severity:** Violation
-
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
-
-Privileged containers can much more easily obtain root on the node.
-As such, they are not allowed.
-
-### Rego
-
-```rego
-package container_deny_escalation
-
-import data.lib.core
-import data.lib.pods
-
-violation[msg] {
-    pods.containers[container]
-    container_allows_escalation(container)
-
-    msg := core.format(sprintf("%s/%s: Allows privilege escalation", [core.kind, core.name]))
-}
-
-container_allows_escalation(c) {
-    c.securityContext.allowPrivilegeEscalation == true
-}
-
-container_allows_escalation(c) {
-    core.missing_field(c.securityContext, "allowPrivilegeEscalation")
-}
-```
-
-_source: [../../examples/container-deny-escalation](../../examples/container-deny-escalation)_
-
-## Images must not use the latest tag
-
-**Severity:** Violation
-
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
-
-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.
-
-### Rego
-
-```rego
-package container_deny_latest_tag
-
-import data.lib.core
-import data.lib.pods
-
-violation[msg] {
-    pods.containers[container]
-    has_latest_tag(container)
-
-    msg := core.format(sprintf("%s/%s/%s: Images must not use the latest tag", [core.kind, core.name, container.name]))
-}
-
-has_latest_tag(c) {
-    endswith(c.image, ":latest")
-}
-
-has_latest_tag(c) {
-    contains(c.image, ":") == false
-}
-```
-
-_source: [../../examples/container-deny-latest-tag](../../examples/container-deny-latest-tag)_
-
-## Containers must not run as privileged
-
-**Severity:** Violation
-
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
-
-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.
-
-### Rego
-
-```rego
-package container_deny_privileged
-
-import data.lib.core
-import data.lib.pods
-import data.lib.security
-
-violation[msg] {
-    pods.containers[container]
-    container_is_privileged(container)
-
-    msg = core.format(sprintf("%s/%s/%s: Containers must not run as privileged", [core.kind, core.name, container.name]))
-}
-
-container_is_privileged(container) {
-    container.securityContext.privileged
-}
-
-container_is_privileged(container) {
-    security.added_capability(container, "CAP_SYS_ADMIN")
-}
-```
-
-_source: [../../examples/container-deny-privileged](../../examples/container-deny-privileged)_
-
-## Containers must define resource constraints
-
-**Severity:** Violation
-
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
-
-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.
-
-### Rego
-
-```rego
-package container_deny_without_resource_constraints
-
-import data.lib.core
-import data.lib.pods
-
-violation[msg] {
-    pods.containers[container]
-    not container_resources_provided(container)
-
-    msg := core.format(sprintf("%s/%s/%s: Container resource constraints must be specified", [core.kind, core.name, container.name]))
-}
-
-container_resources_provided(container) {
-    container.resources.requests.cpu
-    container.resources.requests.memory
-    container.resources.limits.cpu
-    container.resources.limits.memory
-}
-```
-
-_source: [../../examples/container-deny-without-resource-constraints](../../examples/container-deny-without-resource-constraints)_
-
-## Pods must not have access to the host aliases
-
-**Severity:** Violation
-
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
-
-Pods that can change aliases in the host's /etc/hosts file can
-redirect traffic to malicious servers.
-
-### Rego
-
-```rego
-package pod_deny_host_alias
-
-import data.lib.core
-import data.lib.pods
-
-violation[msg] {
-    pod_host_alias
-
-    msg := core.format(sprintf("%s/%s: Pod has hostAliases defined", [core.kind, core.name]))
-}
-
-pod_host_alias {
-    pods.pod.spec.hostAliases
-}
-```
-
-_source: [../../examples/pod-deny-host-alias](../../examples/pod-deny-host-alias)_
-
-## Pods must not run with access to the host IPC
-
-**Severity:** Violation
-
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
-
-Pods that are allowed to access the host IPC can read memory of
-the other containers, breaking that security boundary.
-
-### Rego
-
-```rego
-package pod_deny_host_ipc
-
-import data.lib.core
-import data.lib.pods
-
-violation[msg] {
-    pod_has_hostipc
-
-    msg := core.format(sprintf("%s/%s: Pod allows for accessing the host IPC", [core.kind, core.name]))
-}
-
-pod_has_hostipc {
-    pods.pod.spec.hostIPC
-}
-```
-
-_source: [../../examples/pod-deny-host-ipc](../../examples/pod-deny-host-ipc)_
-
-## Pods must not run with access to the host networking
-
-**Severity:** Violation
-
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
-
-Pods that can access the host's network interfaces can potentially
-access and tamper with traffic the pod should not have access to.
-
-### Rego
-
-```rego
-package pod_deny_host_network
-
-import data.lib.core
-import data.lib.pods
-
-violation[msg] {
-    pod_has_hostnetwork
-
-    msg := core.format(sprintf("%s/%s: Pod allows for accessing the host network", [core.kind, core.name]))
-}
-
-pod_has_hostnetwork {
-    pods.pod.spec.hostNetwork
-}
-```
-
-_source: [../../examples/pod-deny-host-network](../../examples/pod-deny-host-network)_
-
-## Pods must not run with access to the host PID namespace
-
-**Severity:** Violation
-
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
-
-Pods that can access the host's process tree can view and attempt to
-modify processes outside of their namespace, breaking that security
-boundary.
-
-### Rego
-
-```rego
-package pod_deny_host_pid
-
-import data.lib.core
-import data.lib.pods
-
-violation[msg] {
-    pod_has_hostpid
-
-    msg := core.format(sprintf("%s/%s: Pod allows for accessing the host PID namespace", [core.kind, core.name]))
-}
-
-pod_has_hostpid {
-    pods.pod.spec.hostPID
-}
-```
-
-_source: [../../examples/pod-deny-host-pid](../../examples/pod-deny-host-pid)_
-
-## Pods must run as non-root
-
-**Severity:** Violation
-
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
-
-Pods running as root (uid of 0) can much more easily escalate privileges
-to root on the node. As such, they are not allowed.
-
-### Rego
-
-```rego
-package pod_deny_without_runasnonroot
-
-import data.lib.pods
-import data.lib.core
-
-violation[msg] {
-    pods.pod
-    not pod_runasnonroot
-
-    msg := core.format(sprintf("%s/%s: Pod allows running as root", [core.kind, core.name]))
-}
-
-pod_runasnonroot {
-    pods.pod.spec.securityContext.runAsNonRoot
-}
-```
-
-_source: [../../examples/pod-deny-without-runasnonroot](../../examples/pod-deny-without-runasnonroot)_
-
-## PodSecurityPolicies must require all capabilities are dropped
-
-**Severity:** Violation
-
-**Resources:** policy/PodSecurityPolicy
-
-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.
-
-### Rego
-
-```rego
-package psp_deny_added_caps
-
-import data.lib.core
-import data.lib.psps
-import data.lib.security
-
-violation[msg] {
-    not psp_dropped_all_capabilities
-
-    msg := core.format(sprintf("%s/%s: Does not require droping all capabilities", [core.kind, core.name]))
-}
-
-psp_dropped_all_capabilities {
-    psps.psps[psp]
-    security.dropped_capability(psp, "all")
-}
-```
-
-_source: [../../examples/psp-deny-added-caps](../../examples/psp-deny-added-caps)_
-
-## PodSecurityPolicies must not allow privileged escalation
-
-**Severity:** Violation
-
-**Resources:** policy/PodSecurityPolicy
-
-Allowing privileged containers can much more easily obtain root on the node.
-As such, they are not allowed.
-
-### Rego
-
-```rego
-package psp_deny_escalation
-
-import data.lib.core
-import data.lib.psps
-
-violation[msg] {
-    psps.psps[psp]
-    allows_escalation(psp)
-
-    msg := core.format(sprintf("%s/%s: Allows privilege escalation", [core.kind, core.name]))
-}
-
-allows_escalation(p) {
-    p.spec.allowPrivilegeEscalation == true
-}
-
-allows_escalation(p) {
-    core.missing_field(p.spec, "allowPrivilegeEscalation")
-}
-```
-
-_source: [../../examples/psp-deny-escalation](../../examples/psp-deny-escalation)_
-
-## PodSecurityPolicies must not allow access to the host aliases
-
-**Severity:** Violation
-
-**Resources:** policy/PodSecurityPolicy
-
-Allowing pods to can change aliases in the host's /etc/hosts file can
-redirect traffic to malicious servers.
-
-### Rego
-
-```rego
-package psp_deny_host_alias
-
-import data.lib.core
-import data.lib.psps
-
-violation[msg] {
-    psp_allows_hostaliases
-
-    msg := core.format(sprintf("%s/%s: Allows for managing host aliases", [core.kind, core.name]))
-}
-
-psp_allows_hostaliases {
-    psps.psps[_].spec.hostAliases
-}
-```
-
-_source: [../../examples/psp-deny-host-alias](../../examples/psp-deny-host-alias)_
-
-## PodSecurityPolicies must not allow access to the host IPC
-
-**Severity:** Violation
-
-**Resources:** policy/PodSecurityPolicy
-
-Allowing pods to access the host IPC can read memory of
-the other containers, breaking that security boundary.
-
-### Rego
-
-```rego
-package psp_deny_host_ipc
-
-import data.lib.core
-import data.lib.psps
-
-violation[msg] {
-    psp_allows_hostipc
-
-    msg := core.format(sprintf("%s/%s: Allows for sharing the host IPC namespace", [core.kind, core.name]))
-}
-
-psp_allows_hostipc {
-    psps.psps[_].spec.hostIPC
-}
-```
-
-_source: [../../examples/psp-deny-host-ipc](../../examples/psp-deny-host-ipc)_
-
-## PodSecurityPolicies must not allow access to the host network
-
-**Severity:** Violation
-
-**Resources:** policy/PodSecurityPolicy
-
-Allowing pods to access the host's process tree can view and attempt to
-modify processes outside of their namespace, breaking that security
-boundary.
-
-### Rego
-
-```rego
-package psp_deny_host_network
-
-import data.lib.core
-import data.lib.psps
-
-violation[msg] {
-    psp_allows_hostnetwork
-
-    msg := core.format(sprintf("%s/%s: Allows for accessing the host network", [core.kind, core.name]))
-}
-
-psp_allows_hostnetwork {
-    psps.psps[_].spec.hostNetwork
-}
-```
-
-_source: [../../examples/psp-deny-host-network](../../examples/psp-deny-host-network)_
-
-## PodSecurityPolicies must not allow access to the host PID namespace
-
-**Severity:** Violation
-
-**Resources:** policy/PodSecurityPolicy
-
-Allowing pods to access the host's process tree can view and attempt to
-modify processes outside of their namespace, breaking that security
-boundary.
-
-### Rego
-
-```rego
-package psp_deny_host_pid
-
-import data.lib.core
-import data.lib.psps
-
-violation[msg] {
-    psp_allows_hostpid
-
-    msg = core.format(sprintf("%s/%s: Allows for sharing the host PID namespace", [core.kind, core.name]))
-}
-
-psp_allows_hostpid {
-    psps.psps[_].spec.hostPID
-}
-```
-
-_source: [../../examples/psp-deny-host-pid](../../examples/psp-deny-host-pid)_
-
-## PodSecurityPolicies must require containers to not run as privileged
-
-**Severity:** Violation
-
-**Resources:** policy/PodSecurityPolicy
-
-Allowing privileged containers can much more easily obtain root on the node.
-As such, they are not allowed.
-
-### Rego
-
-```rego
-package psp_deny_privileged
-
-import data.lib.core
-import data.lib.psps
-
-violation[msg] {
-    psp_allows_privileged
-
-    msg := core.format(sprintf("%s/%s: Allows for privileged workloads", [core.kind, core.name]))
-}
-
-psp_allows_privileged {
-    psps.psps[_].spec.privileged
-}
-```
-
-_source: [../../examples/psp-deny-privileged](../../examples/psp-deny-privileged)_
-
-## Roles must not allow use of privileged PodSecurityPolicies
-
-**Severity:** Violation
-
-**Resources:** rbac.authorization.k8s.io/Role
-
-Workloads not running in the exempted namespaces must not use PodSecurityPolicies with privileged permissions.
-
-### Rego
-
-```rego
-package role_deny_use_privileged_psps
-
-import data.lib.core
-import data.lib.rbac
-import data.lib.security
-
-violation[msg] {
-    role_uses_privileged_psp
-
-    msg := core.format(sprintf("%s/%s: Allows using PodSecurityPolicies with privileged permissions", [core.kind, core.name]))
-}
-
-role_uses_privileged_psp {
-    rule := core.resource.rules[_]
-    rbac.rule_has_resource_type(rule, "podsecuritypolicies")
-    rbac.rule_has_verb(rule, "use")
-    rbac.rule_has_resource_name(rule, privileged_psps[_].metadata.name)
-}
-
-privileged_psps[psp] {
-    psp := data.inventory.cluster["policy/v1beta1"].PodSecurityPolicy[_]
-    psp_is_privileged(psp)
-}
-
-psp_is_privileged(psp) {
-    psp.spec.privileged
-}
-
-psp_is_privileged(psp) {
-    security.added_capability(psp, "SYS_ADMIN")
-}
-```
-
-_source: [../../examples/role-deny-use-privileged-psp](../../examples/role-deny-use-privileged-psp)_
-
-## Deprecated Deployment and DaemonSet API
-
-**Severity:** Warning
-
-**Resources:** apps/DaemonSet apps/Deployment
-
-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`.
-
-### Rego
-
-```rego
-package any_warn_deprecated_api_versions
-
-import data.lib.core
-
-warn[msg] {
-    resources := ["DaemonSet", "Deployment"]
-    core.apiVersion == "extensions/v1beta1"
-    core.kind == resources[_]
-
-    msg := core.format(sprintf("API extensions/v1beta1 for %s has been deprecated, use apps/v1 instead.", [core.kind]))
-}
-```
-
-_source: [../../examples/any-warn-deprecated-api-versions](../../examples/any-warn-deprecated-api-versions)_
-
-## Containers should not have a writable root filesystem
-
-**Severity:** Warning
-
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
-
-In order to prevent persistence in the case of a compromise, it is
-important to make the root filesystem read-only.
-
-### Rego
-
-```rego
-package container_warn_no_ro_fs
-
-import data.lib.core
-import data.lib.pods
-
-warn[msg] {
-    pods.containers[container]
-    no_read_only_filesystem(container)
-
-    msg := core.format(sprintf("%s/%s/%s: Is not using a read only root filesystem", [core.kind, core.name, container.name]))
-}
-
-no_read_only_filesystem(container) {
-    core.has_field(container.securityContext, "readOnlyRootFilesystem")
-    not container.securityContext.readOnlyRootFilesystem
-}
-
-no_read_only_filesystem(container) {
-    core.missing_field(container.securityContext, "readOnlyRootFilesystem")
-}
-```
-
-_source: [../../examples/container-warn-no-ro-fs](../../examples/container-warn-no-ro-fs)_
-
-## PodSecurityPolicies should require that a read-only root filesystem is set
-
-**Severity:** Warning
-
-**Resources:** policy/PodSecurityPolicy
-
-Allowing pods to access the host's network interfaces can potentially
-access and tamper with traffic the pod should not have access to.
-
-### Rego
-
-```rego
-package psp_warn_no_ro_fs
-
-import data.lib.core
-import data.lib.psps
-
-warn[msg] {
-    psps.psps[psp]
-    no_read_only_filesystem(psp)
-
-    msg := core.format(sprintf("%s/%s: Allows for a writeable root filesystem", [core.kind, core.name]))
-}
-
-no_read_only_filesystem(psp) {
-    core.missing_field(psp.spec, "readOnlyRootFilesystem")
-}
-
-no_read_only_filesystem(psp) {
-    not psp.spec.readOnlyRootFilesystem
-}
-```
-
-_source: [../../examples/psp-warn-no-ro-fs](../../examples/psp-warn-no-ro-fs)_
diff --git a/test/lib/libraryA.rego b/test/lib/libraryA.rego
new file mode 100644
index 0000000..7557d72
--- /dev/null
+++ b/test/lib/libraryA.rego
@@ -0,0 +1,3 @@
+package lib.libraryA
+
+import data.lib.libraryB
diff --git a/test/lib/libraryB.rego b/test/lib/libraryB.rego
new file mode 100644
index 0000000..8f16cb0
--- /dev/null
+++ b/test/lib/libraryB.rego
@@ -0,0 +1,1 @@
+package lib.libraryB
diff --git a/test/src.rego b/test/src.rego
new file mode 100644
index 0000000..8f359b7
--- /dev/null
+++ b/test/src.rego
@@ -0,0 +1,12 @@
+# @title The title
+#
+# The description
+#
+# @kinds apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+package test
+
+import data.lib.libraryA
+
+violation[msg] {
+    true
+}
diff --git a/test/template_Test.yaml b/test/template_Test.yaml
new file mode 100755
index 0000000..6306a8c
--- /dev/null
+++ b/test/template_Test.yaml
@@ -0,0 +1,27 @@
+apiVersion: templates.gatekeeper.sh/v1beta1
+kind: ConstraintTemplate
+metadata:
+  creationTimestamp: null
+  name: test
+spec:
+  crd:
+    spec:
+      names:
+        kind: Test
+  targets:
+  - libs:
+    - |-
+      package lib.libraryA
+
+      import data.lib.libraryB
+    - package lib.libraryB
+    rego: |-
+      package test
+
+      import data.lib.libraryA
+
+      violation[msg] {
+          true
+      }
+    target: admission.k8s.gatekeeper.sh
+status: {}