con un clic
con un clic
Create a new product in ComplianceAsCode project
Create Automatus test scenarios to test the given rule.
Search for existing rules that match a given requirement text. Identify rules that implement a specific control.
Create or update a versioned profile pair (versioned + unversioned extends pattern).
Build a ComplianceAsCode product
Create a new security rule with all required components
| name | create-template |
| description | Create a template for checks and remediations |
| argument-hint | template-name |
Create a new template in shared/templates/. Templates generate checks (OVAL) and remediations (Bash, Ansible, etc.) from parameters, reducing code duplication across similar rules.
Template name: $ARGUMENTS
Prefer mcp__content-agent__* tools when available. When the MCP server is not configured, use the filesystem-based alternatives noted as Fallback in each step. See .claude/skills/shared/mcp_fallbacks.md for detailed fallback procedures.
shared/templates/<name>/.oval/shared.xml, bash/shared.sh). The build system prefers static content over templated content when both exist.template: key in rule.yml that generates content from a template instead of using static content.Read the Template Reference to understand existing templates, their parameters, and structure.
Use mcp__content-agent__list_templates to get the full list of available templates.
Fallback: Run ls shared/templates/ to list template directories.
Verify the template name $ARGUMENTS:
lowercase_with_underscores (no hyphens, no uppercase)Use mcp__content-agent__get_template_schema with template_name=$ARGUMENTS to check if the name already exists.
Fallback: Check if shared/templates/$ARGUMENTS/ already exists.
If the name already exists, inform the user and stop.
Discuss and agree on the following with the user:
lowercase_with_underscores)mcp__content-agent__search_rules with a query matching the template's purpose.
Fallback: grep -rl in linux_os/guide/ or applications/openshift/ for similar patterns.Add the new template to Template Reference, matching the existing format exactly:
#### $ARGUMENTS
- Description of what the template checks/remediates.
- Parameters:
- **param_name** - description of the parameter
- **optional_param** - (optional) description. Default: `"default_value"`.
- Languages: Ansible, Bash, OVAL
Insert the entry in alphabetical order among the existing templates, before the "Creating Templates" section.
All template files go in shared/templates/$ARGUMENTS/.
template.ymlList all languages the template supports. The build system only generates content for languages listed here.
Valid language identifiers: anaconda, ansible, bash, blueprint, bootc, ignition, kickstart, kubernetes, oval, puppet, sce-bash.
supported_languages:
- ansible
- bash
- oval
template.pyThis Python file preprocesses template parameters before they reach the Jinja2 engine. Every template must have a template.py with a preprocess(data, lang) function, even if it only returns data unchanged.
The function:
data: a dictionary of template parameters from the rule's template.vars, plus the implicit _rule_id keylang: the language being generated (e.g., "oval", "bash", "ansible")data dictionary (a missing return silently breaks preprocessing)Use the preprocessor to:
test_correct_value, test_wrong_value)langYou can import helper functions from ssg.utils (e.g., ssg.utils.escape_id()).
template.pydef preprocess(data, lang):
return data
template.py with Defaults, Validation and Test Datadef preprocess(data, lang):
# Validate required parameters
if "key" not in data:
raise ValueError(
"Rule {0} is missing required 'key' parameter".format(
data["_rule_id"]))
# Set defaults for optional parameters
if "separator" not in data:
data["separator"] = "="
# Language-specific transformations
if lang == "oval":
data["escaped_key"] = data["key"].replace(".", "_")
# Create test scenario parameters
data["test_correct_value"] = str(data.get("value", "correct_value"))
data["test_wrong_value"] = "wrong_value"
return data
Common patterns in existing preprocessors:
package_installed: validates evr format with regexkey_value_pair_in_file: sets defaults for sep, sep_regex, prefix_regex, app, creates test_correct_value/test_wrong_valuesysctl: uses ssg.utils.escape_id() for ID sanitization, detects IPv6 from variable name, generates test values based on datatypeservice_disabled: sets packagename and daemonname defaults from servicenameThe build system injects these variables into every template (available in both template.py and .template files):
| Variable | Description |
|---|---|
_rule_id | The rule ID of the rule using the template |
rule_id | Same as _rule_id, available in Jinja context |
rule_title | The rule's title from rule.yml |
product | Current product being built (e.g., rhel9) |
pkg_system | Package system for the product (e.g., rpm, dpkg) |
Casing note: _rule_id is a template parameter, so it becomes _RULE_ID in .template files (like all parameters). The others (rule_id, rule_title, product, pkg_system) are environment/context variables that stay lowercase. In practice, both {{{ _RULE_ID }}} and {{{ rule_id }}} resolve to the same rule ID — use whichever is conventional for the context (existing templates typically use _RULE_ID in OVAL id= attributes and rule_id in macro arguments).
For each language in template.yml, create a <lang>.template file. The file name must match the language identifier exactly.
This project uses custom Jinja2 delimiters (defined in ssg/jinja.py). Standard Jinja2 syntax will NOT work:
{{{ expr }}} (triple braces), NOT {{ expr }}{{% stmt %}}, NOT {% stmt %}{{# comment #}}, NOT {# comment #}Critical — variable casing in .template files:
rule.yml vars (and those set in template.py) must be in UPPERCASE. The build system converts all parameter keys to uppercase automatically. Example: package_name in rule.yml becomes PACKAGE_NAME in the template.rule_id, rule_title, product, pkg_system) stay lowercase._rule_id is a parameter (becomes _RULE_ID), while rule_id is a context variable (stays lowercase). Both resolve to the same value.Bash and Ansible templates must start with these metadata comments:
# platform = multi_platform_all
# reboot = false
# strategy = configure
# complexity = low
# disruption = low
Adjust platform to match the template's applicability. Use specific platforms (e.g., multi_platform_rhel,multi_platform_fedora) if the template only applies to certain products.
bash.template)Use macros from shared/macros/bash.jinja for common operations:
# platform = multi_platform_all
# reboot = false
# strategy = configure
# complexity = low
# disruption = low
{{{ bash_replace_or_append(CONFIG_FILE, KEY, VALUE) }}}
Common Bash macros:
{{{ bash_package_install(package=PKGNAME) }}}{{{ bash_package_remove(package=PKGNAME) }}}{{{ bash_replace_or_append(config_file, key, value) }}}{{{ bash_instantiate_variables(XCCDF_VARIABLE) }}}{{{ set_config_file(path, key, value, ...) }}}ansible.template)Use macros from shared/macros/ansible.jinja:
# platform = multi_platform_all
# reboot = false
# strategy = configure
# complexity = low
# disruption = low
- name: Ensure {{{ KEY }}} is set to {{{ VALUE }}}
ansible.builtin.lineinfile:
path: "{{{ CONFIG_FILE }}}"
regexp: '^{{{ KEY }}}'
line: "{{{ KEY }}}={{{ VALUE }}}"
state: present
create: true
Common Ansible macros:
{{{ ansible_instantiate_variables(XCCDF_VARIABLE) }}}{{{ ansible_set_config_file(msg, path, key, value, ...) }}}oval.template)OVAL templates use shorthand format. Use macros from shared/macros/oval.jinja:
<def-group>
<definition class="compliance" id="{{{ _RULE_ID }}}" version="1">
{{{ oval_metadata("Description of what is checked.", affected_platforms=["multi_platform_all"], rule_title=rule_title) }}}
<criteria>
<criterion comment="check description"
test_ref="test_{{{ _RULE_ID }}}" />
</criteria>
</definition>
<!-- Add OVAL tests, objects, and states here -->
</def-group>
Common OVAL macros:
{{{ oval_metadata(description, affected_platforms, rule_title) }}}{{{ oval_test_package_installed(package, evr, test_id) }}}{{{ oval_check_config_file(path, parameter, value, ...) }}}Use Jinja conditionals for product-specific behavior:
{{% if product in ["sle12", "sle15"] %}}
# SUSE-specific implementation
{{% elif product in ["ubuntu2204", "ubuntu2404"] %}}
# Ubuntu-specific implementation
{{% else %}}
# Default implementation
{{% endif %}}
Templates that support XCCDF variables typically branch on whether the variable is set:
{{% if XCCDF_VARIABLE %}}
{{{ bash_instantiate_variables(XCCDF_VARIABLE) }}}
# Use $XCCDF_VARIABLE
{{% else %}}
# Use hardcoded VALUE
{{% endif %}}
Create templated test scenarios in shared/templates/$ARGUMENTS/tests/. These tests are automatically inherited by all rules using the template.
Every template needs at minimum:
Common test scenarios:
correct_value.pass.sh — correct configuration is presentwrong_value.fail.sh — incorrect value is configuredmissing_value.fail.sh — required configuration is absentmissing_file.fail.sh — configuration file does not existduplicate_values.pass.sh — multiple correct entries (if applicable)conflicting_values.fail.sh — correct and incorrect entries both present (if applicable)commented_value.fail.sh — correct value is commented out (if applicable)Test scenarios are Bash scripts with Jinja2 templating. They use the same custom delimiters as .template files, and template parameters must be in UPPERCASE.
Use test-specific parameters created in template.py (e.g., TEST_CORRECT_VALUE, TEST_WRONG_VALUE) rather than the rule's actual values — this ensures tests work correctly across all rules using the template.
Pass scenario example (correct_value.pass.sh):
#!/bin/bash
mkdir -p $(dirname {{{ PATH }}})
echo "{{{ KEY }}}{{{ SEP }}}{{{ TEST_CORRECT_VALUE }}}" > "{{{ PATH }}}"
Fail scenario example (wrong_value.fail.sh):
#!/bin/bash
mkdir -p $(dirname {{{ PATH }}})
echo "{{{ KEY }}}{{{ SEP }}}{{{ TEST_WRONG_VALUE }}}" > "{{{ PATH }}}"
XCCDF variable handling in tests:
#!/bin/bash
{{%- if XCCDF_VARIABLE %}}
# variables = {{{ XCCDF_VARIABLE }}}={{{ TEST_CORRECT_VALUE }}}
{{% endif %}}
mkdir -p $(dirname {{{ PATH }}})
echo "{{{ KEY }}}{{{ SEP }}}{{{ TEST_CORRECT_VALUE }}}" > "{{{ PATH }}}"
The # variables = VAR_NAME=value comment tells the test framework to set the XCCDF variable to the specified value during the test.
description.pass.sh or description.fail.sh{{{ bash_package_install(PKGNAME) }}}) when availablesed to remove existing config entries)Ask the user for a rule ID to use with the template.
If no suitable rule exists, suggest using the create-rule skill to create one.
Add the template key to the rule's rule.yml:
template:
name: $ARGUMENTS
vars:
param1: value1
param2: value2
Use @product syntax for product-specific parameter values:
template:
name: $ARGUMENTS
vars:
param1: value1
param1@rhel9: different_value
param1@ubuntu2404: another_value
If the rule has static content (files in oval/, bash/, ansible/ subdirectories), those files override templated content. Remove any static files that the template now generates, unless the rule intentionally needs static content to override the template for a specific language.
Build the product to verify the template generates correctly:
./build_product <product> --datastream --rule-id <rule_id>
Verify correct template expansion in the rendered output:
Use mcp__content-agent__get_rendered_rule with product=<product> and rule_id=<rule_id>.
Fallback: Inspect build/<product>/rules/<rule_id>/ for generated content.
Run tests using the test-rule skill to verify that both checks and remediations work correctly.
Report to the user:
/build-product <product> to build"/test-rule <rule_id> to run tests".template files. The build system converts parameter keys to uppercase automatically..template file exists but the language is not listed in template.yml. Every supported language must be listed there.preprocess function in template.py must return data. A missing return statement silently breaks preprocessing.oval/shared.xml) and a template, static content takes priority. Remove static files when switching to a template.{{{ }}} for expressions, {{% %}} for statements. Standard Jinja2 {{ }} / {% %} will NOT work.template.py (e.g., TEST_CORRECT_VALUE) instead of the rule's actual values to ensure tests work across all rules using the template.After completion, the template directory should look like:
shared/templates/$ARGUMENTS/
template.yml # Supported languages
template.py # Parameter preprocessing
ansible.template # Ansible remediation (if supported)
bash.template # Bash remediation (if supported)
oval.template # OVAL check (if supported)
tests/
correct_value.pass.sh # At least 1 pass scenario
wrong_value.fail.sh # At least 1 fail scenario
... # Additional scenarios