Open Policy Agent (OPA) provides Rego, an open source policy language with versatile resource inspection features for determining cloud security posture. In Datadog, you can write custom rules with Rego to control the security of your infrastructure.
Defining a rule starts with a Rego policy, defined inside a module. Cloud Security Misconfigurations uses a module template like the one below to simplify writing rules:
packagedatadogimportdata.datadog.outputasdd_outputimportfuture.keywords.containsimportfuture.keywords.ifimportfuture.keywords.ineval(resource_type)="skip"if{# Logic that evaluates to true if the resource should be skipped}else="pass"{# Logic that evaluates to true if the resource is compliant}else="fail"{# Logic that evaluates to true if the resource is not compliant}# This part remains unchanged for all rulesresultscontainsresultif{someresourceininput.resources[input.main_resource_type]result:=dd_output.format(resource,eval(resource))}
Take a close look at each part of this module to understand how it works.
The first line contains the declaration package datadog. A package groups Rego modules into a single namespace, allowing modules to be imported safely. Currently, importing user modules is not a feature of custom rules. All posture management rules are grouped under the datadog namespace. For your results to be returned properly, group your rules under the package datadog namespace.
The next three statements import the OPA-provided keywords contains, if, and in. These keywords allow defining rules with more expressive syntax to improve readability. Note: Importing all keywords with import future.keywords is not recommended.
importdata.datadog.outputasdd_output
The next line imports the Datadog helper method, which formats your results to the specifications of the Datadog posture management system. datadog.output is a Rego module with a format method that expects your resource as the first argument, and a string, pass, fail, or skip as the second argument, describing the outcome of the inspection of your resource.
The rule evaluates the resource, and provides the outcome as a string depending on the state of the resource. You can change the order of pass, fail, and skip according to your needs. The rule above has fail as a default, if skip_me and should_pass are false or nonexistent in your resource. Alternatively, you can make pass the default:
The final section of the template module builds your set of results:
# This part remains unchanged for all rulesresultscontainsresultif{someresourceininput.resources[input.main_resource_type]result:=dd_output.format(resource,eval(resource))}
This section passes through all resources from the main resource type and evaluates them. It creates an array of results to be processed by the posture management system. The some keyword declares the local variable resource, which comes from the array of main resources. The eval rule is executed on every resource, returning a pass, fail, or skip. The dd_output.format rule formats the resource and evaluation correctly to be processed by cloud security.
This section of the policy does not need to be modified. Instead, when you select your main resource type in the Choose your main resource type dropdown when cloning rules, it is inserted in this section of the policy. You can also access the array of your resources through input.resources.some_resource_type, replacing some_resource_type with the main resource type that you chose, for example, gcp_iam_policy.
The template helps you start writing custom rules. You aren’t required to follow it. You can instead clone an existing default rule, or you write your own rule from scratch. However, for the posture management system to interpret your results, they must be called results in your Rego module and be formatted as follows:
The above rule example evaluates basic true or false flags like should_pass in your resource. Consider a rule that expresses a logical OR, for example:
This skips the resource if it has no port attribute, and fails it if it falls within one of the two “bad” ranges.
Sometimes you want to examine more than one resource type in your rule. To do this, you can select some related resource types in the dropdown under Advanced Rule Options. You can then access the arrays of related resources through input.resources.related_resource_type, replacing related_resource_type with whatever related resource you would like to access.
When writing a policy for more than one resource type, it can be time consuming to loop through all instances of a related resource type for each main resource. Take the following example:
eval(iam_service_account)="fail"if{somekeyininput.resources.gcp_iam_service_account_keykey.parent==iam_service_account.resource_namekey.key_type=="USER_MANAGED"}else="pass"{true}# This part remains unchanged for all rulesresultscontainsresultif{someresourceininput.resources[input.main_resource_type]result:=dd_output.format(resource,eval(resource))}
This rule determines whether there are any instances of gcp_iam_service_account_key that are user managed and match to a gcp_iam_service_account (the resource selected as the main resource type). If the service account has a key that is user managed, it produces a fail result. The eval rule is executed on every service account, and loops through every service account key to find one that matches the account, resulting in a complexity of O(MxN), where M is the number of service accounts and N is the number of service account keys.
To improve the time complexity significantly, build a set of key parents that are user managed with a set comprehension: