Skip to content

Commit

Permalink
Merge pull request #431 from yuumasato/workshop-jq-section
Browse files Browse the repository at this point in the history
Extend workshop with a section about `jq` filters
  • Loading branch information
openshift-ci[bot] authored Oct 13, 2023
2 parents 980c106 + 142915f commit 00312c7
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 17 deletions.
22 changes: 13 additions & 9 deletions doc/tutorials/workshop/content/exercises/09-writing-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ so let's focus instead on writing content for OpenShift.

There is a handy tool in the `utils` directory that will help you create such
rules and test them locally or against an existing cluster: [`
./utils/add_platform_rule.py`](
https://github.com/ComplianceAsCode/content/blob/master/utils/add_platform_rule.py).
./utils/add_kubernetes_rule.py`](
https://github.com/ComplianceAsCode/content/blob/master/utils/add_kubernetes_rule.py).

Let's take it into use!

Expand Down Expand Up @@ -52,7 +52,7 @@ value.

```
$ source .pyenv.sh # Configure PYTHONPATH for CaC modules
$ ./utils/add_platform_rule.py create platform\
$ ./utils/add_kubernetes_rule.py create platform\
--rule must_have_compliant_cm \
--name my-compliance-configmap --namespace openshift --type configmap \
--title "Must have compliant CM" \
Expand Down Expand Up @@ -116,7 +116,7 @@ $ oc project openshift-compliance
We can test the rule as follows:

```
$ ./utils/add_platform_rule.py cluster-test --rule must_have_compliant_cm
$ ./utils/add_kubernetes_rule.py cluster-test --rule must_have_compliant_cm
```

This command will:
Expand All @@ -139,7 +139,7 @@ accept values such as `yessss` or `yeah`. In this case, we'll need to adjust our
little:

```
$ ./utils/add_platform_rule.py create platform\
$ ./utils/add_kubernetes_rule.py create platform\
--rule must_have_compliant_cm \
--name my-compliance-configmap --namespace openshift --type configmap \
--title "Must have compliant CM" \
Expand All @@ -162,7 +162,7 @@ $ grep operation applications/openshift/must_have_compliant_cm/rule.yml
Let's test it out and see that the pattern still matches:

```
$ ./utils/add_platform_rule.py cluster-test --rule must_have_compliant_cm
$ ./utils/add_kubernetes_rule.py cluster-test --rule must_have_compliant_cm
...
* The result is 'COMPLIANT'
```
Expand All @@ -180,7 +180,7 @@ $ oc patch -n openshift configmap my-compliance-configmap \
And let's verify that it still matches:

```
$ ./utils/add_platform_rule.py cluster-test --rule must_have_compliant_cm
$ ./utils/add_kubernetes_rule.py cluster-test --rule must_have_compliant_cm
...
* The result is 'COMPLIANT'
```
Expand All @@ -190,12 +190,16 @@ For completeness, lets modify the `ConfigMap` to have a non-compliant value and
```
$ oc patch -n openshift configmap my-compliance-configmap \
-p '{"data": {"compliant": "hehehe nope"}}' --type=merge
$ ./utils/add_platform_rule.py cluster-test --rule must_have_compliant_cm
$ ./utils/add_kubernetes_rule.py cluster-test --rule must_have_compliant_cm
...
* The result is 'NON-COMPLIANT'
```

Once you've tested your rule and feel its in a good shape, you should fill in the missing
parameters from the template so they'll appear nicely in the report. Finally, you can add
the rule to a relevant profile in the `ocp4/profiles/` directory, build it, upload it to a
the rule to a relevant profile in the `ocp4/profiles/` directory, or a control file in
`controls/` directory, build it, upload it to a
`ProfileBundle` and take it into use as part of your regular compliance scans!

In the next section we will learn how to make a rule more flexible to our needs
[with variables](10-rule-parametrization.md).
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ EOF
Then run the following command to change the rule to use the variable
we just created:
```
$ ./utils/add_platform_rule.py create platform\
$ ./utils/add_kubernetes_rule.py create platform\
--rule must_have_compliant_cm \
--name my-compliance-configmap --namespace openshift --type configmap \
--title "Must have compliant CM" \
Expand All @@ -90,14 +90,14 @@ $ oc patch -n openshift configmap my-compliance-configmap \
-p '{"data": {"compliant": "yep"}}' --type=merge
```
```
$ ./utils/add_platform_rule.py cluster-test --rule must_have_compliant_cm
$ ./utils/add_kubernetes_rule.py cluster-test --rule must_have_compliant_cm
...
* The result is 'COMPLIANT'
```

### Testing Rules with Profile Tailorings

So far we have been using the `./utils/add_platform_rule.py` script to test
So far we have been using the `./utils/add_kubernetes_rule.py` script to test
our rule. It creates very specific `ComplianceScans` that cannot cover all the use
cases.

Expand Down Expand Up @@ -187,3 +187,25 @@ my-own-profile DONE COMPLIANT

Our rule is ready to be enabled in multiple profiles checking different values
in each `Profile`.

To check that rule fails as expected, let's change the `ConfigMap` to an incompliant value:
```
$ oc patch -n openshift configmap my-compliance-configmap \
-p '{"data": {"compliant": "nope"}}' --type=merge
configmap/my-compliance-configmap patched
```

Then manually start a re-scan:
`$ oc annotate compliancescan/my-own-profile compliance.openshift.io/rescan=`

And follow the scan status and check that is indeed not compliant:
```
$ oc get scan -w
NAME PHASE RESULT
my-own-profile RUNNING NOT-AVAILABLE
my-own-profile AGGREGATING NOT-AVAILABLE
my-own-profile AGGREGATING NOT-AVAILABLE
my-own-profile DONE NON-COMPLIANT
```

Next we will learn what are [node rules](11-node-rules.md) and how do they differ from platform rules.
8 changes: 5 additions & 3 deletions doc/tutorials/workshop/content/exercises/11-node-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ owned by root in our cluster's nodes.

To create this node rule, execute the following `create node` command:
```
$ ./utils/add_platform_rule.py create node \
$ ./utils/add_kubernetes_rule.py create node \
--rule file_owner_etc_system_release \
--title "File /etc/system-release must be owned by root" \
--description "We need to ensure that root owns the system release file" \
Expand All @@ -47,7 +47,7 @@ input arguments.
Here is another example of how to quickly generate a a node rule that checks
the sysctl `kernel.randomize_va_space` value:
```
$ ./utils/add_platform_rule.py create node \
$ ./utils/add_kubernetes_rule.py create node \
--rule sysctl_kernel_randomize_va_space \
--title "Ensure ASLR is fully enabled" \
--description "Make it harder to exploit vulnerabilities by employing full address space layout randomization" \
Expand All @@ -57,7 +57,7 @@ $ ./utils/add_platform_rule.py create node \

### Selecting the nodes to check

The node rules created with `./utils/add_platform_rule.py create node ...`
The node rules created with `./utils/add_kubernetes_rule.py create node ...`
are by default applicable to all nodes in the cluster, i.e.: `worker` and
`master` nodes.

Expand Down Expand Up @@ -85,3 +85,5 @@ One such example is the Kubelet configuration in each node. The

Check the rule [kubelet_enable_cert_rotation](https://github.com/ComplianceAsCode/content/blob/master/applications/openshift/kubelet/kubelet_enable_cert_rotation/rule.yml)
for an example of how the `yamlfile_value` template is used.

Let's now take a look at the [most common types of rules](12-common-rules.md) and the templates used when writing rules for Kubernetes.
7 changes: 5 additions & 2 deletions doc/tutorials/workshop/content/exercises/12-common-rules.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
Title: All types of rules
PrevPage: 11-node-rules
NextPage: ../finish
NextPage: 13-complex-yaml.md
---
Common types of rules
===================
Expand Down Expand Up @@ -31,7 +31,7 @@ This type of check is handled by the [yamlfile_value](https://complianceascode.r
template.

You can write a rule and make use of the template by yourself, or
use the `./utils/add_platform_rule.py` script showcased in past sections.
use the `./utils/add_kubernetes_rule.py` script showcased in past sections.
Note that more advanced uses of the template will require you to write the
input data manualy, check the template's documentation.

Expand Down Expand Up @@ -110,3 +110,6 @@ template:
datatype: int
```
Check rule [kubelet_enable_protect_kernel_sysctl_kernel_panic_on_oops](https://github.com/ComplianceAsCode/content/blob/master/applications/openshift/kubelet/kubelet_enable_protect_kernel_sysctl_kernel_panic_on_oops/rule.yml) for a complete example.

In the next section we will look at a way to handle [more complex checks](13-complex-yaml.md)
and resources.
141 changes: 141 additions & 0 deletions doc/tutorials/workshop/content/exercises/13-complex-yaml.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
---
Title: Checking complex yaml structures
PrevPage: 12-common-rules.md
NextPage: ../finish
---

Checking complex yaml structures
====================

The `yamlfile_value` template is great to check if a yaml key exists or not,
and it does exist, if it has a specific value.

But some of the resources and configurations of a Kubernetes cluster can be
defined in quite a complex way, and just the yaml path syntax used by the
template may not be sufficient to get to the value we want to assess.

For this reason the CaC/content rules used by the Compliance Operator can
leverage [`jq`](https://jqlang.github.io/jq/) to better select what needs to be assessed.
A `jq` filter allows us to pre-process the resources and configurations so that
they are easier to check with the `yamlfile_value` template.

There are two use cases where a `jq` filter is necessary.

## 1. Checking a key in a nested yaml or json

Some Kuberentes configurations contain a yaml or json formatted value in them,
and checking for these values requires the use of `jq` filter.

For example, looking at an `openshift-kube-api-server` `ConfigMap` we can see
a `data."config.yaml"` key whose value is yaml formatted.

```
$ oc get configmap config -n openshift-kube-apiserver -oyaml
apiVersion: v1
data:
config.yaml: '{"admission":{"pluginConfig":{"PodSecurity":{"configuration":{"apiVersion":"pod-security.admission.conf
ig.k8s.io/v1","defaults":{"audit":"restricted","audit-version":"latest","enforce":"privileged","enforce-version":"lates
t","warn":"restricted","warn-version":"latest"},"exemptions":{"usernames":["system:serviceaccount:openshift-infra:build
-controller"]},"kind":"PodSecurityConfiguration"}},"network.openshift.io/ExternalIPRanger":{"configuration":{"allowIngr
essIP":false,"apiVersion":"network.openshift.io/v1","externalIPNetworkCIDRs":null,"kind":"ExternalIPRangerAdmissionConf
ig"},"location":""},"network.openshift.io/RestrictedEndpointsAdmission":{"configuration":{"apiVersion":"network.openshi
ft.io/v1","kind":"RestrictedEndpointsAdmissionConfig","restrictedCIDRs":["10.128.0.0/14","172.30.0.0/16"]}}}},"apiServe
rArguments":{"allow-privileged":["true"],"anonymous-auth":["true"],"api-audiences":["https://kubernetes.default.svc"],"
audit-log-format":["json"],"audit-log-maxbackup":["10"],"audit-log-maxsize":["200"],"audit-log-path":["/var/log/kube-ap
iserver/audit.log"],"audit-policy-file":["/etc/kubernetes/static-pod-resources/configmaps/kube-apiserver-audit-policies
...
kind: ConfigMap
metadata:
creationTimestamp: "2023-10-06T12:59:31Z"
name: config
namespace: openshift-kube-apiserver
resourceVersion: "21469"
uid: fc192e71-282a-4af6-9492-87324ea83410
```

To check the value of `audit-log-maxbackup`, which is in the `config.yaml` key,
we need to use the `jq` query to select the value and "extract" it for us.

Rule [api_server_audit_log_maxbackup](https://github.com/ComplianceAsCode/content/blob/master/applications/openshift/api-server/api_server_audit_log_maxbackup/rule.yml)
is evaluating the resource listed above.
The rule's `jq` filter is `.data."config.yaml" | fromjson'` and its `yamlpath` is `.apiServerArguments["audit-log-maxbackup"][:]`.
The Compliance Operator will fetch the resource and pass down for checking only the data that came out from the `jq` filter.

![Diagram of resource colection and check with and without a jq filter](images/jqfilter_preprocessing.png)

For this section, we will create a very simple `ConfigMap` with embedded yaml and check one of its keys.

So let's create a machine config that has a nested yaml value:
```
$ cat << EOF | oc create -f -
apiVersion: v1
kind: ConfigMap
metadata:
name: my-nested-compliance-configmap
namespace: openshift
data:
my-config.yaml: '{foo: bar, nested-key: nested-compliant}'
EOF
```

Now let's write a rule to check whether the value of `nested-key` is `nested-compliant`.

The script `utils/add_kubernetes_rule.py` provides an easy way to create a rule that uses `jq` filters.
Just pass the option `--jqfilter` with the desired filter.
```
$ ./utils/add_kubernetes_rule.py create platform \
--rule check_nested_yaml \
--name my-nested-compliance-configmap --namespace openshift --type configmap \
--title "Check value of nested-key in my-config-yaml" \
--description "It is important that the nested configmap has a nested-key with value nested-compliant." \
--match-entity "at least one" \
--match "nested-compliant" \
--yamlpath ".nested-key" \
--jqfilter '.data."my-config.yaml"'
```

Just like when we were creating the other rules, the tool lays down a `rule.yml` file with the same keys filled out.
There are two diffences this time around:
* First, is the warning messsage. Which is now defined by a different macro.
This macro adds the `jq` filter to the rule, which is parsed by the operator when collecting the resource.
* Second difference is the `filepath` key in the template, which is define with the help of macro too.
This macro ensures that a unique `filepath` is set for this resource when it is collected.

You can test the rule with:
```
$ ./utils/add_kubernetes_rule.py cluster-test --rule check_nested_yaml
* Testing rule check_nested_yaml in-cluster
* Ensuring openshift-compliance namespace exists.
...
* Running scan with rule 'check_nested_yaml'
> Output from last phase check: LAUNCHING NOT-AVAILABLE
...
> Output from last phase check: RUNNING NOT-AVAILABLE
...
> Output from last phase check: AGGREGATING NOT-AVAILABLE
> Output from last phase check: DONE COMPLIANT
* The result is 'COMPLIANT'
```

If you'd like to test that the rule fails with incompliant values, patch the `ConfigMap` with an incompliant value, and run the test again.
```
$ oc patch -n openshift configmap my-nested-compliance-configmap -p '{"data": {"my-config.yaml": "{foo: bar, nested-key: nested-not-compliant}"}}
configmap/my-nested-compliance-configmap patched
$ ./utils/add_kubernetes_rule.py cluster-test --rule check_nested_yaml
...
* The result is 'NON-COMPLIANT'
```

## 2. Filtering the data to have simpler yamlpaths

This is a generalization of the first use case, `jq` filters can be used to filter and select the data
fetched by the operator.

When the resource being checked is extensive or complex, `jq` is invaluable for simplifying the data before it is
passed down to be evaluated with `yamlpath` in `yamlfile_value` template.

Rule [api_server_encryption_provider_cipher](https://github.com/ComplianceAsCode/content/blob/master/applications/openshift/api-server/api_server_encryption_provider_cipher/rule.yml)
is one example of a rule that filters the data for a simpler `yamlpath`.

And rule [configure_network_policies_hypershift_hosted](https://github.com/ComplianceAsCode/content/blob/master/applications/openshift/networking/configure_network_policies_hypershift_hosted/rule.yml)
is an example of a rule that uses `jq` filters to select attributes from an array to be evaluated.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 00312c7

Please sign in to comment.