Mondoo cnspec is an open source scanner that evaluates the security of IT assets such as Kubernetes clusters, cloud environments, data stores, and networks. IT professionals use cnspec to find security issues that expose their systems to ransomware, data theft, and other attacks.
This sample teaches devops and security engineers how to create custom security policies for cnspec scans.
Policy Authoring Guide for cnspec:
Write Custom Policies
Policies are the specifications that cnspec uses when it scans a system for misconfigurations that put your business at risk. Think of a policy as a checklist that cnspec relies on to ensure that a system is secure. In Mondoo and cnspec, these collections of security requirements are expressed as highly readable code.
Policy as code
Security policies and compliance frameworks typically are documents. Text describes each guideline and its rationale, and sometimes the consequences of not complying.
But documents don’t check your environments. The work to verify that your infrastructure follows security standards is often manual, time intensive, and error prone. For example, if you need to manually demonstrate compliance for an audit, it can take weeks just to provide a snapshot of a single moment in time.
Policy as code lets you automate compliance using security benchmarks and best practices. The code serves two purposes: It documents the security guidelines and it tests your systems to ensure they follow those guidelines.
cnspec policies and policy bundles
Each cnspec policy is codified as a collection of checks that test for certain configuration settings. For example, the Mondoo Linux Security – Users and Groups policy includes these checks:
- There are no users in the root group.
- No duplicate user names exist.
- All system accounts are non-login.
Policy bundles are YAML files that contain at least one policy. They group related policies. For example, the Mondoo Linux Security policy bundle contains a Configure SSH Server policy that is specific to Linux, a Logging policy that is specific to Linux, and other policies that define secure Linux practices.
Mondoo provides dozens of free policy bundles that cover the most common types of assets. Mondoo Platform has even more. If your organization has unique needs that these policy bundles don’t meet, you can create custom policy bundles.
Find policy bundles in Mondoo’s cnspec-policies GitHub repo.
A very simple policy bundle
All cnspec policies are stored in YAML files. These files are called bundles because they combine policies together. Their filename ends in .mql.yaml. To learn more about policies and policy bundles, read About Policies.
Here’s a very simple example of a policy bundle. It contains only one policy, Simple example policy 1:
policies:
- uid: simple-example1
name: Simple example policy 1
version: "1.0.0"
scoring_system: highest impact
authors:
- name: Lunalectric
email: security@lunalectric.com
docs:
desc: |-
Descriptive documentation about this policy
groups:
- title: group1
checks:
- uid: sshd-01
title: Ensure the port is set to 22
mql: sshd.config.params["Port"] == 22
impact: 30
- uid: sshd-02
title: Prevent weaker CBC ciphers from being used
mql: sshd.config.ciphers.none( /cbc/ )
impact: 60
queries:
- uid: sshd-d-1
title: Gather SSH config params
mql: sshd.config.params
We’ll use this simple policy bundle example to explore how to write a policy.
Basic policy attributes
| The attribute… | Defines… |
| uid | A unique identifier for the policy. |
| name | A descriptive name for the policy. |
| version | The current version of the policy.(We recommend using semantic versioning to keep track of major and minor policy changes.) |
| scoring_system | How Mondoo calculates the score for an asset. To learn more, read Score Policies. |
| authors | The person or entity to credit for writing the policy, and email where they can be reached. |
| docs | Optional documentation section for describing the policy’s purpose and makeup. |
| groups | The checks and queries that cnspec uses to assess an asset’s security. |
Queries
A query is an MQL inquiry that requests information about an asset. For example, a query can ask what version of an OS is running on a container or request the UIDs, names, and statuses of all users in an application.
Queries in a policy add helpful insights to scan report output. (They’re also the bases for checks, which are described below.)
The simple example policy bundle above contains one query. It requests the configuration values of the SSH server scanned. cnspec includes the information in the scan report output.
| The attribute… | Defines… |
| uid | A unique identifier for the query. |
| title | A descriptive name for the query. |
| mql | The MQL query that requests information, such as the number of root accounts or the state of a port. |
To learn how to create MQL queries, read Write Effective MQL.
Checks
An MQL query that also makes an assertion is called a check. Checks retrieve a value just like any query. For example, a check can ask What OS version is running? How they differ from other queries is that they compare the retrieved value to a desired value and return a true/false result based on that comparison.
For example, a check can assert that the OS version value should be 8.3.1 or higher. All checks return a Boolean true or false. In our example, if the current OS version on the scanned asset is 8.2, the check returns false. If the current OS version is 8.3.1 or 8.3.5, the check returns true.
Checks are the building blocks of policies. A typical policy identifies a number of desired configurations (such as MFA is enabled or no ports are publicly accessible) and instructs Mondoo to compare them to the actual configuration on the scan target.
The simple example policy bundle above contains two checks:
- The first check ensures the SSH port is set to 22.
- The second check ensures that SSH uses a strong cipher.
Each check has its own attributes:
| The attribute… | Defines… |
| uid | A unique identifier for the check. |
| title | A descriptive name for the check that’s useful in report output. |
| mql | The MQL assertion that identifies the desired condition or configuration, such as logging is enabled or encryption is required. |
| impact | How important (on a scale from 0 to 100) the check is in the scope of the entire policy. The impact and result of each check determine the asset’s score on the policy. To learn more, read Score Policies. |
To learn how to create MQL queries and checks, read Write Effective MQL.
Score Policies
cnspec assigns each scanned asset a risk score that summarizes how well it compares to the checks in the policy. Risk scores are based on numeric scores between 0 and 100. These are the ranges for Mondoo risk scores:
| From | To | Risk score | Description |
| 90 | 100 | Critical | Presents extreme risk to your organization |
| 70 | 89 | High | Presents significant risk to your organization |
| 40 | 69 | Medium | Presents moderate risk to your organization |
| 1 | 39 | Low | Presents little risk to your organization |
| 0 | 0 | None | Presents no risk to your organization |
The score is based on the number of checks that return a true value (pass) compared to how many return a false value (fail).
When assessing the overall security of an asset, some checks may be more important than others. For example, suppose a strong cipher is more important to your organization than SSH using port 22. You can use the impact attribute to give more importance to one check and less importance to another check.
In our sample policy, the Ensure the port is set to 22 check has an impact of 90 and the Prevent weaker CBC ciphers from being used check has an impact of 80:
policies:
- uid: simple-example1
name: Simple example policy 1
version: "1.0.0"
scoring_system: highest impact
authors:
- name: Lunalectric
email: security@lunalectric.com
docs:
desc: |-
Descriptive documentation about this policy
groups:
- title: group1
checks:
- uid: sshd-01
title: Ensure the port is set to 22
mql: sshd.config.params["Port"] == 22
impact: 90
- uid: sshd-02
title: Prevent weaker CBC ciphers from being used
mql: sshd.config.ciphers.none( /cbc/ )
impact: 80
queries:
- uid: sshd-d-1
title: Gather SSH config params
mql: sshd.config.params
How Mondoo uses these values to calculate an asset’s score depends on the scoring_system setting. You can choose from these scoring systems:
- Average
- Weighted average
- Highest failed impact
Average scoring system
The average scoring system considers impact before averaging check scores. Failed checks with higher impact lower an overall score more than checks with lower impact. This is how the average scoring system calculates the overall score:
- If a check passes (returns true), the asset receives a 0 for that check.
- If a check fails (returns false), the asset receives the impact value for that check. For example, if an asset fails a check with an impact of 90, it receives a 90 for that check.
Our simple example query above contains:
- A port check (sshd-01) with an impact of 90
- A cipher check (sshd-02) with an impact of 80
These are the possible asset scores on this policy:
| Port check (impact 90) | Cipher check (impact 80) | Overall risk score |
| Pass (0) | Pass (0) | (0 + 0) / 2 = 0 or No risk |
| Pass (0) | Fail (80) | (0 + 80) / 2 = 40 or Medium |
| Fail (90) | Pass (0) | (90 + 0) / 2 = 45 or Medium |
| Fail (90) | Fail (80) | (90 + 80) / 2 = 85 or High |
To use the average scoring system, set the scoring system value to average:
policies:
- uid: simple-example1
name: Simple example policy 1
version: "1.0.0"
scoring_system: average
Weighted average scoring system
The weighted average scoring system is just like the average scoring system except that it also considers another factor when calculating an asset’s score: the weight value assigned to a check. This scoring system gives checks with higher weight greater influence over an asset’s total score than checks with lower weight. To learn about assigning weights to checks, talk with your Mondoo support team.
Note: The weighted average scoring system can produce overly positive results for assets that fail checks with very high impact scores.
To use the weighted average scoring system, set the scoring system value to weighted:
policies:
- uid: simple-example1
name: Simple example policy 1
version: "1.0.0"
scoring_system: weighted
Highest failed impact scoring system
The highest impact scoring system only considers the highest-impact failing check in the policy. It relies on the same method of subtraction as the average scoring system: It uses the impact value as the overall score.
Our example policy includes one check with an impact score of 90 and another with an impact score of 80.
| Port check (impact 90) | Cipher check (impact 80) | Overall risk score |
| Pass (0) | Pass (0) | 0 or No risk |
| Pass (0) | Fail (80) | 80 or High |
| Fail (90) | Pass (0) | 90 or Critical |
| Fail (90) | Fail (80) | 90 or Critical |
To use the highest impact scoring system, set the scoring system value to highest impact:
policies:
- uid: simple-example1
name: Simple example policy 1
version: "1.0.0"
scoring_system: highest impact
Scoring and multiple policies
When Mondoo evaluates an asset based on more than one policy, it uses a weighted average to calculate a single asset score. It weights the individual policy scores based on the number of checks in the policies.
For example, suppose Mondoo assesses an asset based on two policies:
- Policy X contains 100 checks.
- Policy Y contains 20 checks.
If an asset scores 72 on policy X and scores 50 on policy Y:
- Multiply policy x score by 100 because the policy contains 100 checks.
72 x 100 = 7200 - Multiply policy y score by 20 because the policy contains 20 checks.
50 x 20 = 1000 - Divide the sum of the two policies by the total number of checks in both policies.
(7200 + 1000) / 120 = 68 (Medium)
Break up a Policy into Groups / Chapters
A group is a collection of related checks and queries in a policy. Groups are a way of breaking up a policy into more manageable sections.
A common way to use groups is to match the chapters in a written benchmark, text policy, or other compliance document. For each chapter in the document, you can create a group in the policy.
Here’s another simple example of a policy bundle containing one policy:
policies:
- uid: example-with-chapters
name: Simple example with chapters
version: "1.0.0"
scoring_system: highest impact
authors:
- name: Lunalectric
email: security@lunalectric.com
groups:
- title: SSH
checks:
- uid: sshd-01
title: Ensure the port is set to 22
mql: sshd.config.params["Port"] == 22
impact: 30
- uid: sshd-02
title: Prevent weaker CBC ciphers from being used
mql: sshd.config.ciphers.none( /cbc/ )
impact: 60
queries:
- uid: sshd-d-1
title: Gather SSH config params
mql: sshd.config.params
- title: Packages
checks:
- uid: pkg-01
title: Ensure AIDE is installed
mql: package("aide").installed
impact: 70
- uid: pkg-02
title: Ensure prelink is disabled
mql: package("prelink").installed == false
impact: 7
The policy contains two groups:
- The SSH group has two checks and one query, all concerning SSH parameters.
- The Packages group contains two checks, both of which concern installed packages.
A policy can have as many groups as you need.
Reuse Queries and Checks
Within a policy bundle, you can reuse queries and checks.
Here’s another simple example of a policy bundle:
policies:
- uid: luna1
name: Lunalectric policy 1
version: "1.0.0"
scoring_system: highest impact
authors:
- name: Lunalectric
email: security@lunalectric.com
docs:
desc: |-
Descriptive documentation about this policy
groups:
- title: test
checks:
- uid: sshd-01
title: Ensure the port is set to 22
mql: sshd.config.params["Port"] == 22
impact: 30
- uid: sshd-02
title: Prevent weaker CBC ciphers from being used
mql: sshd.config.ciphers.none( /cbc/ )
impact: 60
- uid: shared1
queries:
- uid: sshd-d-1
title: Gather SSH config params
mql: sshd.config.params
- uid: luna2
name: Luna policy 2
version: "1.0.0"
scoring_system: highest impact
authors:
- name: Lunalectric
email: security@lunalectric.com
groups:
- title: test2
checks:
- uid: sshd-03
title: Ensure SSH protocol is set to 2
mql: sshd.config.params["Protocol"] == 2
impact: 50
- uid: shared1
queries:
- uid: shared1
title: Enable strict mode
mql: sshd.config.params["StrictModes"] == "yes"
impact: 70
Multiple policies in a bundle
Policy bundles can contain any number of policies. You write them in the policies section of the bundle. The example above has two policies: Luna policy 1 and Luna policy 2.
Reuse queries and checks in a policy bundle
Notice that the example policy bundle above has a main section at the end named queries. It’s at the same level in the hierarchy as the policies section. This is the shared queries and checks section, intended for items that you use more than once. Here you can put queries and checks that you want to include in multiple policies. Instead of writing the same query or check twice or ten times in many policies, you can write it once, store it in this shared queries section, and simply reference it in any policy where you want to include it.
In the example policy bundle above, there’s one shared item in the shared queries section: Enable strict mode. The shared item’s UID is shared1. Both policies reference it (include it in their checks) using its shared1 UID.
Tip: The shared queries main section of a policy bundle can contain both queries that only collect information and checks (queries that make assertions and produce scores when the scan runs).
Limit Target Assets with Filters
Filters can specify what target assets a policy, group, check, or query can run against. A filter is simply a condition, written in MQL, that must be met. If the condition is true, cnspec proceeds with the assessment. If the condition is false, cnspec skips the assessment.
Any fields you can query about any resources can be the basis for a filter. The most common basis for filters is platform information. For example, you can add a filter that tells cnspec to run a policy only on AWS EKS clusters. Or you can add a filter that tells cnspec to run a check only on certain versions of an operating system.
Tip: Filters are an essential part of creating variants. To learn about variants, read Make Policies Flexible with Variants.
Apply a filter to a check or query
Add filters information to a check or query to apply a filter to it.
This is an example of a check with a filter:
- uid: ssh-root-login-is-disabled
title: Ensure SSH root login is disabled
filters: package('openssh-server').installed
impact: 90
mql: sshd.config.params["PermitRootLogin"] == "no"
The filter in the ssh-root-login-is-disabled check tells cnspec to run the check only on assets that have the SSH Server package installed. When scanning an asset without SSH Server, cnspec skips this check.
Apply a filter to a chapter or group
Add filters information to a group to apply a filter to it.
This is an example of a chapter type of group with two filters:
groups:
- title: AWS Compute Services
type: chapter
filters: |
asset.name == "aws"
asset.kind == "api"
checks:
...
Unless the asset is an AWS compute service, cnspec skips all the checks and queries in this group when scanning the asset.
More examples of filters
This filter limits scans to only GCP projects:
asset.platform == "gcp-project"
This filter limits scans to only kubelets:
asset.family.contains('linux')
processes.where( executable == /kubelet/ ).list != []
To learn how to write your own filters, read Write Effective MQL and the MQL Reference.
Define Properties
Properties are an optional method of defining the ideal values for checks. Instead of defining a value in the check itself, you can define it in a property and reference that property in the check. Multiple checks in a policy can share a single property.
This policy does not use properties. It checks that you have strong IAM policies in AWS:
policies:
- uid: no-properties-example
name: Example policy without properties
version: "1.0.0"
authors:
- name: Lunalectric
email: security@lunalectric.com
groups:
- title: group01
checks:
- uid: aws-iam-01
title: Require long passwords
mql: aws.iam.accountPasswordPolicy['MinimumPasswordLength'] >= 8
- uid: aws-iam-02
title: Require uppercase characters
mql: aws.iam.accountPasswordPolicy['RequireUppercaseCharacters'] == true
- uid: aws-iam-03
title: Limit password age
mql: aws.iam.accountPasswordPolicy['MaxPasswordAge'] <= 90
The no-properties-example policy above performs three checks:
- Whether the minimum password length is set to 8 or higher.
- Whether uppercase letters are required in passwords.
- Whether passwords expire after 90 or fewer days.
In each of these checks, the ideal value that the policy checks against is in the check itself.
An alternate way to structure these checks is to put all the ideal values in properties. You define properties separately from the checks themselves—similar to defining variables.
This policy shows how you can use properties to achieve the same results as the no-properties-example policy:
policies:
- uid: example-with-properties
name: Example policy using properties
version: "1.0.0"
authors:
- name: Lunalectric
email: security@lunalectric.com
groups:
- title: group01
checks:
- uid: aws-iam-01
title: Require long passwords
mql: aws.iam.accountPasswordPolicy['MinimumPasswordLength'] >= props.passwordMinLength
- uid: aws-iam-02
title: Require uppercase character
mql: aws.iam.accountPasswordPolicy['RequireUppercaseCharacters'] == props.passwordUppercase
- uid: aws-iam-03
title: Require password rotation
mql: aws.iam.accountPasswordPolicy['MaxPasswordAge'] <= props.passwordMaxAge
props:
- uid: passwordMinLength
title: Minimum password length
mql: "8"
- uid: props.passwordUppercase
title: Whether to require at least one uppercase character in passwords
mql: "true"
- uid: props.passwordMaxAge
title: Maximum time that a user can go without changing their password
mql: "90"
In the example-with-properties policy above, the three checks refer to properties for the ideal values to check against. The props section of the policy assigns a value to each of the three properties.
Use one property for multiple checks
Multiple checks in a policy can share a single property. This can make updates easier when your organization’s requirements change.
As a simple example, suppose you create a policy that checks IAM best practices across multiple platforms. Even though the platforms are different, your company’s minimum password length requirement is the same. If you create password length checks for each different platform, you don’t need to define the minimum password length value multiple times. Instead, all of the password length checks can point to a single property. That way, there’s only one value to change when your company’s minimum password length requirement changes.
Make Policies Flexible with Variants
Variants are checks that behave differently based on conditions you define. They’re alternative versions of checks.
For example, suppose you want to ensure that Remote Desktop Protocol (RDP) is restricted from the internet. You want to perform this check both in GCP projects and in Terraform files. You can do this by creating one variant for GCP projects and another for Terraform files:
- The GCP variant queries if the asset is a GCP project and checks RDP access using the GCP resource.
- The Terraform variant queries if the asset is a Terraform file and checks RDP access using the Terraform resource.
- If the asset is neither a GCP project nor a Terraform file, cnspec doesn’t execute an RDP check.
Filters for variants
cnspec relies on filters to determine which variant to run against an asset. A filter is a condition written in MQL. Any fields you can query about any resources can be the basis for a filter.
To learn more about filters, read Limit Target Assets with Filters.
Create variants
Create a variants section in a check to define its variants. This section tells cnspec that the check is made up of variants, and what those variants are.
policies:
- uid: okta-security-example-with-variants
name: Example of a policy that uses variants
version: "1.0.0"
scoring_system: highest impact
authors:
- name: Lunalectric
email: security@lunalectric.com
checks:
- uid: password-minimum-length
title: Minimum password length
impact: 30
variants:
- uid: password-minimum-length-runtime
- uid: password-minimum-length-terraform-hcl
- uid: password-minimum-length-terraform-plan
- uid: password-minimum-length-terraform-state
- uid: password-minimum-length-runtime
title: Minimum password length - runtime variant
filters: asset.platform == "okta-org"
impact: 30
mql: |
okta.policies.password.all( settings['password']['complexity']['minLength'] >= 15 )
- uid: password-minimum-length-terraform-hcl
title: Minimum password length - Terraform HCL variant
filters: asset.platform == "terraform-hcl" && terraform.providers.one( nameLabel == "okta" )
impact: 30
mql: |
terraform.resources.where( nameLabel == /okta_policy_password/ ).all( arguments['password_min_length'] == /var/ || arguments['password_min_length'] >= 15 )
- uid: password-minimum-length-terraform-plan
title: Minimum password length - Terraform plan variant
filters: asset.platform == "terraform-plan" && terraform.plan.resourceChanges.contains( providerName == /okta/ )
impact: 30
mql: |
terraform.plan.resourceChanges.where( type == /okta_policy_password/ ).all( change.after['password_min_length'] >= 15 )
- uid: password-minimum-length-terraform-state
title: Minimum password length - Terraform state variant
filters: asset.platform == "terraform-state" && terraform.state.resources.contains( type == /okta_policy_password/ )
impact: 30
mql: |
terraform.state.resources.where( type == /okta_policy_password/ ).all( values['password_min_length'] >= 15 )
The variants section in the okta-security-example-with-variants policy establishes the variants for the password-minimum-length check. These are the four variants:
- cnspec runs the password-minimum-length-runtime variant only on one condition: The asset is an Okta organization.
- cnspec runs the password-minimum-terraform-hcl variant only if the asset is an Okta Terraform HCL file.
- cnspec runs the password-minimum-terraform-plan variant if the asset is an Okta Terraform plan.
- cnspec runs the password-minimum-terraform-state variant if the asset is an Okta Terraform state.
Use one property for multiple variants
Often you use variants to ensure that different types of assets have one common property, as in the example above. All of the variants in the okta-security-example-with-variants policy check that the minimum password length is 15; they just check the value using different resources for different assets.
For efficiency and easier maintenance, you can write all four variants to use one property instead of defining 15 multiple times:
policies:
- uid: okta-security-example-with-variants
name: Example of a policy that uses variants
version: "1.0.0"
scoring_system: highest impact
authors:
- name: Lunalectric
email: security@lunalectric.com
checks:
- uid: password-minimum-length
title: Minimum password length
impact: 30
variants:
- uid: password-minimum-length-runtime
- uid: password-minimum-length-terraform-hcl
- uid: password-minimum-length-terraform-plan
- uid: password-minimum-length-terraform-state
- uid: password-minimum-length-runtime
title: Minimum password length - runtime variant
filters: asset.platform == "okta-org"
impact: 30
mql: |
okta.policies.password.all( settings['password']['complexity']['minLength'] >= props.minPass )
- uid: password-minimum-length-terraform-hcl
title: Minimum password length - Terraform HCL variant
filters: asset.platform == "terraform-hcl" && terraform.providers.one( nameLabel == "okta" )
impact: 30
mql: |
terraform.resources.where( nameLabel == /okta_policy_password/ ).all( arguments['password_min_length'] == /var/ || arguments['password_min_length'] >= props.minPass )
- uid: password-minimum-length-terraform-plan
title: Minimum password length - Terraform plan variant
filters: asset.platform == "terraform-plan" && terraform.plan.resourceChanges.contains( providerName == /okta/ )
impact: 30
mql: |
terraform.plan.resourceChanges.where( type == /okta_policy_password/ ).all( change.after['password_min_length'] >= props.minPass )
- uid: password-minimum-length-terraform-state
title: Minimum password length - Terraform state variant
filters: asset.platform == "terraform-state" && terraform.state.resources.contains( type == /okta_policy_password/ )
impact: 30
mql: |
terraform.state.resources.where( type == /okta_policy_password/ ).all( values['password_min_length'] >= props.minPass )
Props:
- uid: minPass
title: Minimum password length
mql: "15"
To learn more about properties, read Define Properties.
Next Steps in Policy Authoring
If you’re not already skilled at writing MQL checks and queries, we recommend that you use the cnspec shell to experiment with MQL. In the interactive shell, it’s easy to test and troubleshoot a check or query until you perfect it.
To learn about the MQL language, read Write Effective MQL.
To explore the thousands of different resources that MQL can query, read the MQL Reference.
Once you’re comfortable creating checks and queries, we recommend that you download an existing policy bundle and use it as a foundation to create your own. You can find many examples of policy bundles in Mondoo’s cnspec-policies GitHub repo.
To check for errors in the policy bundles you create, run:
cnspec bundle lint BUNDLE-NAME.mql.yaml
For BUNDLE-NAME, substitute the name of your policy bundle file.
Get Help
Can’t find the answers you need?
Open source users: Join our community discussion on GitHub.
Mondoo Platform users: Join our community Slack channel to chat with us and other Mondoo users.
