MQL

Power up MQL

The freshly baked cnquery and cnspec v10 release includes some exciting new language features in MQL. This blog post dives into these capabilities and the use cases that motivated them.

Recurse

Imagine you have a large JSON object with users scattered throughout it. They may be found across different paths:

{
  "serviceUsers": [
    {
      "name": "bob",
      "service": "clicker"
    },
    ...
  ],
  "org": {
    "admins": [
      { 
        "name": "joy",
        "teamMembers": [ ... ]
      }
  ... 

Given the above example, how can we find every user in this object? With the new recurse method this question can be answered easily.

Imagine that structure is parsed into a jdata object.

jdata = parse.json("data.json").params;

Given that users have a name field defined, we could look for those across all objects and paths:

> jdata.recurse( name != empty )
[
  0: {
	name: "bob",
service: "clicker"
  },
  1: {
	name: "joy",
	teamMembers: [ ... ]
  },
  ...
]

From here, you can search or modify results further. For example, we could be looking for all users that have teamMembers defined:

> jdata.recurse( name != empty ).where( teamMembers != empty )

We could also grab fields for further processing:

> jdata.recurse( name != empty ).map( name )
[
  0: "bob"
  1: "joy"
  ...
]

Monitor your infrastructure for security misconfigurations and maps those checks automatically to top compliance frameworks.

Named function arguments

MQL generally maps all fields you access to the object that is calling it. For example, when you use keywords like where, their filter function is bound to each object in the list:

> users.where( name != /bob/i )

Implicitly, we are calling name on each user object that the list looks at. Under the hood, the inner function looks like this:

user.name != /bob/i

MQL only had two ways to access this bound object. One was the implicit _ identifier, which points to the bound object:

> users.where( _.name != /bob/i )

The second way only applies to maps and is called via key or value to access their data:

> {name: "bob", id: "bid"}.where( value == "bob" )
{
  name: "bob"
}

However, we noticed that an important use case wasn't covered by these methods: nested recursion.

For example, imagine you have a list of groups and want to find entries whose members contained a user with the same name as the group name. Thanks to named function arguments, we can now cover this use case:

> groups.where(g: g.members.contains(u: u.name == g.name))
groups.where.list: [
  0: group name="root" gid=0
]

Let's take this apart: We are looking for a set of groups that match our criteria. The outer loop contains:

> groups.where(g: g.members.contains(u: u.name == g.name))
groups.where.list: [
  0: group name="root" gid=0
]

Here we define the argument g, which means that whenever we call this identifier, we are accessing the group that the loop is looking at.

Next, we look at the members of each group g:

groups.where(g:
  ... 
)

The members field is a list of users that are assigned to this group. Again, we define a named argument, in this case u, which allows us to access each user in this list.

Finally, we add the filter condition and make use of the group g and the member user u in each loop:

u.name == g.name

If we bring this all together with nice formatting, we get:

groups.where(g:
  g.members.contains(u:
    u.name == g.name
  )
)

The use of named function arguments isn't limited to nested loops. You can use it anywhere in MQL to help with the readability of the code:

packages.where(package:
  package.name == /ssh/i
)

Semver

One of the most requested features in MQL, as early as our first alpha releases, was intelligent operations for comparing versions. It took us some time to get here, but we finally have native support for semantic versions in MQL.

To use semantic versions, you can wrap any string with the new semver keyword

> semver("1.2.3")
semver: 1.2.3

Semver is very useful when comparing versions with each other. Without semantic versions, we often get unexpected results like this:

> "1.2.3" < "1.12"
# false

With semantic versions, however, this behaves correctly:

> semver("1.2.3") < semver("1.12")
# true

We have also added support for automatically converting strings to semver if only one side of the operation is a string.

Dominik Richter

Dom is a founder, coder, and hacker and one of the creators of Mondoo. He helped shape the DevOps and security space with projects like InSpec and Dev-Sec.io. Dom worked in security and automation at companies like Google, Chef, and Deutsche Telekom. Beyond his work, he loves to dive deep into hacker and nerd culture, science and the mind, and making colorful pasta from scratch.

You might also like

Linux
Exploring the Latest Security Features in Ubuntu 24.04
Releases
Mondoo Firewatch
Releases
Mondoo March 2024 Release Highlights