ComplianceAsCode
ComplianceAsCode/content (1.9k β) is a tool to create a product-specific XML (ssg-debian11-ds.xml) used by OpenSCAP π.
There are multiple reasons to get involved with the Compliance As Code project. Most of the time, we may want to add support for a new product or for a new guide. Assuming that is what you want to do, the Compliance As Code Docker project might help.
To get started with it β¨:
$ git clone --recursive "https://github.com/ComplianceNinjas/compliance-as-code-docker.git" cac_docker
$ cd cac_docker
$ docker compose up -d
$ docker attach $(docker compose ps -q)
docker$ cd content
docker$ ./build_product debian11 -j $(nproc)
Once you have successfully built an existing product, you might generate your own product using the template.
$ cd loader/.template
$ ./init.sh
$ cd ..
$ nano config.json # product information
$ python3 merge.py # merge files in "content/"
docker$ source .pyenv.sh
docker$ ./utils/after_merge.sh
docker$ ./build_product <your_product_name> -j $(nproc)
$ ./clean.sh # undo merge in "content/"
We merge your files in loader/
into content/
, then generate/update rules, and finally build our product. Inside loader
, we don't have any of the rules automatically edited by scripts with ./utils/after_merge.sh
.
Project Overview
Note that the project is somewhat complex. β οΈ
A control π is a recommendation of a hardening guide, such as Ensure /tmp is a separate partition. One control may contain multiple tasks to perform.
A control file (content/controls/
) π correspond to a compliance guide. It contains some of the compliance metadata along with the list of controls that the guide contains.
A rule (linux_os/guide/
) π corresponds to a reusable task in a control. For instance, partition_for_tmp
checks if /tmp
is on a separate partition. A rule contains metadata and links to a template.
A product (content/products/<product>/
) π§Έ is the target of a compliance guide. It's mostly an operating system such as fedora
(=all versions) or debian11
. It links to a list of profiles.
A profile (content/products/<product>/profiles/
) β
is a list of rules. It may reference some specific rules in a control file (ex: all from level 1).
A template (shared/templates/
) ποΈ is a reusable OVAL check.
An OVAL check (oval.template
, shared.xml
...) π― describes how we can check if a rule was applied.
An applicability check (shared/applicability/
) is used to check if a rule is applicable given the state of the product (package not installed).
Product Configuration
Creating a new product
After generating a new product using init.sh
, you will have:
- A standard profile
standard.profile
- A basic
product.yml
that describes your product - A basic oval check
installed_OS_is_xxx.xml
testing if the tested product is matching the expected product (ex: are we on debian11?)
It automates the process explained in the documentation.
β οΈ If all checks are marked as notapplicable
, it means that the tested OS does not pass the check defined in installed_OS_is_xxx.xml
.
Jinja2
The project uses jinja2 allowing us to use macros and variables inside many files of the project.
Note that, unlike usual jinja
templates, we use one more level of accolades, so {% %}
is now {{% %}}
(ansible-related reason).
Some examples of conditions you might use:
-
"ubuntu" in product
: true if product containsubuntu
-
"ubuntu" not in product
: false if product containsubuntu
-
product in ["debian10", "debian11"]
: true if product in array
Another example: {% if negate %}negate="true" {% endif %}
which optionally shows an OVAL attribute based on a variable negate
.
OVAL Macros can be declared in shared/macros/10-oval.jinja
, or directly inside any OVAL file.
{%- macro some_name(arg, arg2=none) -%}
SOME_OVAL
{%- endmacro -%}
Assuming the macro is within scope (in the general macro file or in the same file), you can call it with:
{{ some_name(arg='xxx') }}
Rule π
A rule links every piece of information related to a single hardening control task. It's described in a rule.yml
. It defines stuff like:
- π± Description (ex: explain what's this rule about)
- π Rule check (ex: to test if the rule was applied)
- π§― Remediation utility (ex: bash script, note...)
- π Applicability check (ex: package is not present οΈβ rule not applicable)
- π Product check (ex: can this rule be used with this product?)
- ...
A rule is stored in a folder. The folder name is the rule id. Rules are stored in groups, e.g. parent folders, which all have a group.yml
. You can place a rule in whatever folder you see fit.
Common rule template
documentation_complete: true
prodtype: xxx,yyy,...
title: 'XXX'
description: XXX
rationale: XXX
severity: medium
platform: machine
references:
xxx: xxx
template:
name: xxx
vars:
- ...
β‘οΈ Refer to the section about rule format.
Rule Description
You can use description
, and rationale
to provide information. They support HTML tags such as:
-
<br />
: new line -
<tt>xxx</tt>
: equivalent of<code>
-
<pre>xxx</pre>
: a line of code - ...
β οΈ If there is a problem with the documentation (ex: <br>
which is a missing auto-closing slash), build will fail at step 9.
prodtype
By default, all rules are compiled for all products. It's problematic as some rules may not be applicable to our product, leading to compilation problems.
Inside each rule, there may be a prodtype
. If there is one, then this rule can only be used (and compiled) by products that were added to it.
You can add your product to each rule prodtype
manually π€ (for the rules you need that have a prodtype), or use a script π€.
β‘οΈ See mod_prodtype.py or autoprodtyper.py.
β οΈ When using autoprodtyper
with a control file, it won't work.
β οΈ If the prodtype
attribute is present, you'll get an error during build (unselects all groups...).
Find rules
To find rules, you can look at other profiles or control files, or you can use find linux_os -name *ftp* -type d 2> /dev/null
(ex: for ftp rules).
platform
By default, if a product uses a rule, the rule is considered to be applicable to it. But that's not always the case. We may require a package to be installed, in a specific environment...
platform: machine and package[ntp]
This rule is only applied if ./shared/applicability/machine.yml
and ./shared/applicability/package.yml
are true.
β‘οΈ Refer to the applicability section.
Controls and Profiles
Controls file
Controls are YAML files representing hardening guides. They are stored in ./controls
. Each control may execute multiple rules.
# ./controls/anssi.yml
- id: R40
title: User authentication running sudo
levels:
- minimal
[...]
rules:
# load a rule by ID
- sudo_remove_nopasswd
- sudo_remove_no_authenticate
Profiles
Each product has a folder ./products/<product_name>/profiles
with available profiles. They are YAML files with the extension .profile
.
documentation_complete: true
title: '...'
description: |-
...
selections:
# load a rule by ID
- sudo_remove_nopasswd
- ...
# load rules from a control file
- anssi:R40 # one specific rule
- anssi:all # all rules
- anssi:all:minimal # only keep if minimal in levels
OVAL Files
OVAL is an XML-based format used by many files in the project. Basic concepts are explained here.
<def-group>
<!-- ... -->
<definition class="..." id="..." version="...">
<criteria>
<criterion test_ref="..." />
</criteria>
</definition>
<!-- body -->
</def-group>
The class
, id
, version
, and test_ref
values are specific to what kind of file you're creating. Other elements are explained here.
criteria
define what to do to pass the check. You can ask for all checks to be true (AND), or only at least one (OR).
<criteria operator="AND" [...] >
<criteria operator="OR" [...] >
A criteria may have children of type criteria
, or criterion
. For the latter, they are referencing the test that will be done.
<criterion test_ref="..." />
<criterion test_ref="..." negate="true" />
π‘ Criterion might not be the only tag that supports negate
.
Tests are tags ending with _test
. They usually have one or two children of type _object
, and _state
respectfully.
<xxx:yyy_test id="test_xxx" check="all" comment="">
<xxx:object object_ref="obj_xxx" />
</xxx:yyy_test>
<xxx:yyy_object id="obj_xxx">
<!-- ... -->
</xxx:yyy_object>
β οΈ Absence of the comment
attribute on a _test
will make the build crash as they are displayed in the HTML report.
OVAL Checks
Link to a rule
A rule may either use a template:
template:
name: your_template_name
vars:
arg1: value1
arg1@product_name: value2
Or, you may add a oval/shared.xml
file inside your rule folder.
β‘οΈ The syntax @product_name
, means that for a specific product, the argument will have a different value.
Template File
There are many existing templates that you can use in your rules. They are located in shared/templates
, see each oval.template
file.
<def-group>
{{{ oval_metadata("XXX") }}}
<definition class="compliance" id="{{{ _RULE_ID }}}" version="3">
<criteria>
<!-- ... -->
</criteria>
</definition>
<!-- ... -->
</def-group>
π Use {{{ ARG1 }}}
to access an argument arg1
passed from a rule.
textfilecontent54
A common tag to test file content.
<ind:textfilecontent54_test id="xxx" check="all" comment="">
<ind:object object_ref="obj_xxx" />
</ind:textfilecontent54_test>
<ind:textfilecontent54_object id="obj_xxx">
<!-- see ind tags section -->
</ind:textfilecontent54_object>
You can use the following attributes on textfilecontent54_test
-
check_existence="all_exist"
: all objects found -
check_existence="none_exist"
: no valid object found
β‘οΈ See also: textfilecontent54_state
.
ind tags
While I'm not sure what's ind
, the following tags are quite handy.
Select a file/folder
You can either give the path:
<ind:filepath>/path/to/file</ind:filepath>
Or, load files in the current folder:
<!-- set current folder -->
<ind:path>/path/to/</ind:path>
<!-- pick one -->
<ind:filename datatype="string">xxx.config</ind:filename>
<ind:filename operation="pattern match">^*\.config$</ind:filename>
Check if a pattern is inside a file
You can "grep" to see if a pattern is inside a file. There are no fancy options like "grep" (case-insensitive, multiple lines...).
<ind:pattern operation="pattern match">some_line_here</ind:pattern>
<ind:pattern operation="pattern match">^some_regex_here$</ind:pattern>
Then, you assert the result you expect:
<ind:instance datatype="int">1</ind:instance>
<ind:instance datatype="int" operation="greater than or equal">1</ind:instance>
<ind:instance datatype="int" operation="equals">1</ind:instance>
β οΈ If the second line is missing, the build will fail.
There are multiple tags that can support a list of values, such as ind:path
. For instance, we can check if at least one file is valid.
Variables and sets
<ind:path var_ref="var_xxx" var_check="at least one" />
<ind:path var_ref="var_xxx" var_check="at least one" datatype="string" />
The variable can be either local
or external
.
A local variable is declared inside the OVAL file.
<constant_variable datatype="string" comment="XXX"
id="var_xxx" version="1">
<value>zzz</value> <!-- one per value -->
</constant_variable>
Support a new package manager
We differentiate package managers (yum, dnf, apt_get, ...) from the package system (rpm, dpkg). Each is mapped to the other.
For instance, let's say we want to add pacman
. On Arch Linux, pacman
is both a package management and system tool. We need to edit files in ./shared/
that are always compiled.
-
applicability/oval/installed_env_has_grub2_package.xml
-
applicability/oval/installed_env_has_login_defs.xml
-
applicability/oval/krb5_server_older_than_1_17_18.xml
-
applicability/oval/krb5_workstation_older_than_1_17_18.xml
-
checks/oval/installed_env_has_zipl_package.xml
-
checks/oval/sshd_version_higher_than_74.xml
[...]
{{% elif pkg_system == "dpkg" %}}
[...]
{{% elif pkg_system == "pacman" %}}
<ind:textfilecontent54_test comment="Do nothing" id="<set the correct id here>" version="1">
</ind:textfilecontent54_test>
{{% endif %}}
[...]
Then, you have to fix macros:
./shared/macros/10-bash.jinja
[...]
{{%- macro bash_pkg_conditional(package, op=None, ver=None) -%}}
[...]
{{%- elif pkg_system == "pacman" -%}}
false
[...]
./shared/macros/10-ocil.jinja
[...]
{{% macro ocil_package(package) -%}}
[...]
{{%- elif pkg_system == "pacman" -%}}
Nothing.
{{%- else -%}}
[...]
{{% macro complete_ocil_entry_package(package) -%}}
[...]
{{%- elif pkg_system == "pacman" %}}
Nothing.
{{%- else -%}}
[...]
./shared/macros/10-oval.jinja
[...]
{{%- macro oval_test_package_removed(package='', test_id='') -%}}
[...]
{{% elif pkg_system == "pacman" %}}
<ind:textfilecontent54_test comment="Do nothing" id="{{{ test_id }}}" version="1">
</ind:textfilecontent54_test>
{{% endif %}}
[...]
{{%- macro oval_test_package_installed(package='', evr='', evr_op='greater than or equal', test_id='') -%}}
[...]
{{% elif pkg_system == "pacman" %}}
<ind:textfilecontent54_test comment="Do nothing" id="{{{ test_id }}}" version="1">
</ind:textfilecontent54_test>
{{% endif %}}
[...]
It should compile now, but you may have to adapt some rules or templates (package managers may be used in applicability or in templates).
β οΈ You will most likely have to edit more project files to completely integrate your package manager/system (remediation...).
π‘ You can look for occurrences in other package managers/systems to find which files to edit.
π» To-do π»
Stuff that I found, but never read/used yet.
- references are used to sort rules in HTML pages
- remediation
- Can change some values in the generated XML
- RCE,
shared.sh
,platform=xx,yyy
- A useful script to learn the coverage of a profile:
$ ./build-scripts/profile_tool.py stats --profile xccdf_org.ssgproject.content_profile_standard --benchmark build/ssg-xxx-xccdf.xml
<extend_definition comment="xxx" definition_ref="yyy" />
<external_variable datatype="int" id="var_xxx" />