Write Custom Opal Policies
Custom Policy Development
Custom policy development generally has four parts:
- Writing the IaC for use as a test input
- Writing the policy
- Writing the policy tests
- Testing the policies
Write Custom Opal Policies
Opal policies are written in Rego and use the same format as Lacework Custom Policies. There are two types of policies: simple policies and advanced policies.
Opal policies must be located in the policies/opal directory.
Simple Policies
Simple policies are useful when the policy only applies to a single resource type and makes a (simple) yes/no decision.
# Policies must always be located right below the `policies` package.
package policies.my_simple_policy
# Simple policies must specify the resource type they will police.
resource_type = "aws_ebs_volume"
# Simple policies must specify `allow` or `deny`. For this example, we use
# an `allow` policy to check that the EBS volume is encrypted.
default allow = false
allow {
input.encrypted == true
}
Custom Error Messages and Attributes (Simple Policies)
To return more information, you can define a custom error message as a simple policy by writing a deny (msg) style policy.
package policies.simple_policy_custom_message
resource_type = "aws_ebs_volume"
deny[msg] {
not input.encrypted
msg = "EBS volumes should be encrypted"
}
Advanced Policies
Advanced policies allow you to observe different kinds of resource types and decide which specific resources are valid or invalid.
# Policies still must be located in the `policies` package.
package policies.user_attached_policy
# Advanced policies typically use functions from the `lacework` library.
import data.lacework.iac
# Set `resource_type` as your desired resource type, such as `AWS::S3::Bucket`, or to `MULTIPLE`.
resource_type = "MULTIPLE"
# `iac.resources` is a function that allows querying for resources of a specific type. In our case, we are just going to ask for the EBS volumes again.
ebs_volumes = iac.resources("aws_ebs_volume")
# Auxiliary function.
is_encrypted(resource) {
resource.encrypted == true
}
# Opal expects advanced policies to contain a `policy` that holds a set of _judgements_.
policy[p] {
resource = ebs_volumes[_]
is_encrypted(resource)
p = iac.allow_resource(resource)
} {
resource = ebs_volumes[_]
not is_encrypted(resource)
p = iac.deny_resource(resource)
}
The Lacework API supports the following functions for advanced policies:
iac.resources(resource_type)returns an object with all resources of the requested type.lacework.input_resource_typesis a set with all resource types in the input document.iac.allow_resource(resource)marks a resource as valid.iac.deny_resource(resource)marks a resource as invalid.iac.deny_resource_with_message(resource, msg)marks a resource as invalid and displays a custompolicy_messagein the output.iac.missing_resource(resource_type)marks a resource as missing. For example, this is useful if you require a log group to be present.iac.missing_resource_with_message(resource_type, msg)marks a resource as missing and displays a custom policy_message in the output.
Custom Error Messages (Advanced Policies)
The functions iac.deny_resource_with_message(resource, msg) and lacework.missing_resource_with_message(resource_type, msg) allow Opal to display a custom policy_message in its output. The following policy demonstrates both functions:
package policies.account_password_policy
import data.lacework.iac
resource_type = "MULTIPLE"
password_policies = iac.resources("aws_iam_account_password_policy")
policy[r] {
password_policy = password_policies[_]
password_policy.minimum_password_length >= 16
r = iac.allow_resource(password_policy)
} {
password_policy = password_policies[_]
not password_policy.minimum_password_length >= 16
msg = "Password policy is too short. It must be at least 16 characters."
r = iac.deny_resource_with_message(password_policy, msg)
} {
count(password_policies) == 0
msg = "No password policy exists."
r = iac.missing_resource_with_message("aws_iam_account_password_policy", msg)
}
The following example policy result demonstrates a missing resource message:
{
"controls": [
"CORPORATE-POLICY_1.1"
],
"filepath": "main.tf",
"input_type": "tf",
"provider": "",
"resource_id": "",
"resource_type": "aws_iam_account_password_policy",
"policy_description": "Per company policy, an AWS account must have a password policy, and it must require a minimum of 16 characters",
"policy_id": "CUSTOM_0001",
"policy_message": "No password policy exists.",
"policy_name": "account_password_policy",
"policy_raw_result": false,
"policy_result": "FAIL",
"policy_severity": "Medium",
"policy_summary": "An AWS account must have a password policy requiring a minimum of 16 characters"
},
Add Policy Metadata
You can add metadata to a policy to enhance Opal's output. To edit metadata, you must create or edit your metadata.yaml file located at <path/to>/policies/opal/<policy>/metadata.yaml. For example:
policies/opal/s3_bucket_is_public
├── metadata.yaml
└── terraform
└── policy.rego
Opal supports the following properties in the metadata.yaml file:
You must specify the following properties:
category: The policy category such as Network or Storage Securitydescription: Longer description of the policyseverity:Critical,High,Medium,Low, orInformationaltitle: Short summary of the policy
The CLI populates the following metadata properties:
sid: The system ID.policyId: The policy ID.
Opal also supports the following metadata properties:
guidelines: A description of the violation, rationale, audit, and remediation.
The following example policy result shows how this metadata looks in the output:
{
"controls": [
"CORPORATE-POLICY_1.1"
],
"families": [
"CORPORATE-POLICY"
],
"filepath": "../opal-ci-example/infra_tf/main.tf",
"input_type": "tf",
"provider": "aws",
"resource_id": "aws_iam_policy.basically_allow_all",
"resource_type": "aws_iam_policy",
"resource_tags": {},
"policy_description": "Per company policy, it is required for all IAM policies to have a description of at least 25 characters.",
"policy_id": "CUSTOM_0001",
"policy_message": "",
"policy_name": "long_description",
"policy_raw_result": false,
"policy_result": "FAIL",
"policy_severity": "Low",
"policy_summary": "IAM policies must have a description of at least 25 characters",
"source_location": [
{
"path": "../opal-ci-example/infra_tf/main.tf",
"line": 6,
"column": 1
}
]
}
CloudFormation, Terraform, Kubernetes, and ARM Policies
CloudFormation policies are written the same way as Terraform policies but require the line input_type := "cfn", as shown in the following simple policy:
package policies.cfn_ebs_volume_encryption
input_type := "cfn"
resource_type := "AWS::EC2::Volume"
default allow = false
allow {
input.Encrypted == true
}
Kubernetes policies require the line input_type := "k8s", as shown in the following policy:
package policies.k8s_job_check
__rego__metadoc__ := {
"id": "K8S_TEST_0123",
"custom": {"severity": "Low"},
"title": "Job containers should not be named `test`",
}
input_type := "k8s"
resource_type := "Job"
default deny = false
deny {
input.spec.template.spec.containers[_].name == "test"
}
ARM policies (in preview) require the line input_type := "arm", as shown in the simple policy below:
package policies.arm_postgresql_tags
__rego__metadoc__ := {
"id": "ARM_POSTGRESQL_001",
"custom": {"severity": "Low"},
"title": "Azure PostgreSQL servers should be tagged 'application:db'",
}
input_type := "arm"
resource_type = "Microsoft.DBforPostgreSQL/servers"
default allow = false
allow {
input.tags.application == "db"
}
Terraform policies do not require input_type to be explicitly set.
The resource_type depends on the input type:
- CloudFormation resource types (For example,
AWS::EC2::Instance) - Terraform AWS, Azure, Google Cloud resource types. For example,
aws_instance. - Kubernetes resource types. For example,
Job. - ARM templates (in preview). For example,
Microsoft.Network/virtualNetworks.