Skip to main content

Write Custom Opal Policies

Custom Policy Development

Custom policy development generally has four parts:

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.

note

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_types is 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 custom policy_message in 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 Security
  • description: Longer description of the policy
  • severity: Critical, High, Medium, Low, or Informational
  • title: 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"
}
note

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.