| name | create-rule |
| description | Create a new security rule with all required components |
Create Rule
Create a new security rule for ComplianceAsCode. This skill handles both templated and non-templated rules.
Rule ID: $ARGUMENTS
Tool Strategy
This skill uses mcp__content-agent__* tools when available (preferred — deterministic, structured results). When the MCP server is not configured, fall back to filesystem-based alternatives noted as Fallback in each step. See .claude/skills/shared/mcp_fallbacks.md for detailed fallback procedures. The skill must complete successfully either way.
Phase 1: Validate Input
-
Validate rule ID format:
- Must be lowercase with underscores (no hyphens or uppercase)
- Example valid IDs:
sshd_max_auth_tries, accounts_password_minlen
-
Check if rule already exists:
Use mcp__content-agent__search_rules with query=$ARGUMENTS to check if a rule with this ID already exists.
Fallback: Use Glob to search for **/$ARGUMENTS/rule.yml.
- If rule exists, inform user and ask if they want to modify it instead
-
Determine rule location:
- Ask user where to create the rule within the guide hierarchy
- Provide suggestions based on rule ID prefix (e.g.,
sshd_* goes under linux_os/guide/services/ssh/ssh_server/)
Phase 2: Determine Rule Type
Use AskUserQuestion to ask the user:
Question: "What type of rule do you want to create?"
Options:
- Templated rule (Recommended) - Uses an existing template for checks and remediations. Faster to create, inherits tests from template.
- Non-templated rule - Custom OVAL, Bash, and Ansible implementations. Requires writing test scenarios.
Phase 3: Template Selection (if templated)
If user chose templated rule:
-
List available templates using mcp__content-agent__list_templates to get all available templates with their descriptions.
Fallback: Run ls shared/templates/ to list available template directories.
-
Present available templates from the list obtained in step 1 and help the user select the right one based on their use case.
-
Ask for template selection using AskUserQuestion
-
Get template parameter schema using mcp__content-agent__get_template_schema with template_name=<selected_template> to get the full parameter schema, supported languages, and documentation.
Fallback: Read shared/templates/<template_name>/template.yml or template.py for parameter definitions. Check existing rules using this template for usage examples.
-
Collect template variables:
- Use the schema from step 4 to identify required and optional parameters
- Ask user for each required variable
Phase 4: Gather Rule Metadata
Collect the following information using AskUserQuestion or prompts:
Required Fields
-
Title: Short descriptive title (displayed in scan results)
- Example: "Disable SSH Root Login"
-
Description: Detailed description using Jinja2 templating
- Can use macros like
{{{ sshd_config_file() }}}, {{{ describe_mount(...) }}}
-
Rationale: Why this rule is important for security
-
Severity: One of low, medium, high, unknown. Default value would be medium.
Identifiers
-
CCE identifiers: Ask which RHEL products need CCEs (rhel8, rhel9, rhel10)
- Format:
cce@rhel9: CCE-XXXXX-X
- Automatic CCE allocation:
- Read available CCEs from
shared/references/cce-redhat-avail.txt
- For each requested product, take the first available CCE from the file
- After adding the CCE to
rule.yml, remove it from cce-redhat-avail.txt to prevent reuse
- If user doesn't want CCEs now, leave blank (can be added later)
head -1 shared/references/cce-redhat-avail.txt
sed -i '1d' shared/references/cce-redhat-avail.txt
References (Optional but Recommended)
- Security references - ask if user has any of:
nist: NIST SP 800-53 controls (e.g., AC-6(2),CM-6(a))
srg: DISA SRG IDs (e.g., SRG-OS-000480-GPOS-00227)
stigid and cis references are filled in automatically by placing the rule into an appropriate control file
Additional Fields
-
Platform (optional): Platform applicability
- Example:
platform: machine (not for containers)
- Example:
platform: package[openssh-server]
-
OCIL (optional): OCIL check description
- Often uses macros like
{{{ complete_ocil_entry_sshd_option(...) }}}
Phase 5: Generate Rule
For Templated Rules
Use mcp__content-agent__generate_rule_from_template with:
template_name: The selected template name
parameters: Template variables collected from the user
rule_id: $ARGUMENTS
product: The target product
This generates the rule directory and rule.yml with the template configuration automatically.
Fallback: Create the rule directory and rule.yml manually using Write tool. Include the template: key with name: and vars: based on the selected template.
For Non-Templated Rules
Use mcp__content-agent__generate_rule_boilerplate with:
rule_id: $ARGUMENTS
title: The rule title
description: The rule description
severity: The severity level
product: The target product
rationale: The rationale (optional)
location: The guide path (optional, e.g., linux_os/guide/services/ssh)
Fallback: Create the directory structure and all files manually using Write tool, following the skeleton templates below.
This generates the rule directory structure with skeleton files:
<location>/$ARGUMENTS/
├── rule.yml
├── oval/
│ └── shared.xml
├── bash/
│ └── shared.sh
├── ansible/
│ └── shared.yml
└── tests/
├── correct.pass.sh
└── wrong.fail.sh
Post-Generation Customization
After MCP generates the boilerplate, customize the generated files:
- Add CCE identifiers, references, and platform applicability to rule.yml
- For non-templated rules, implement the OVAL, Bash, Ansible, and test content
Skeleton Files for Non-Templated Rules
oval/shared.xml:
<def-group>
<definition class="compliance" id="{{{ rule_id }}}" version="1">
{{{ oval_metadata("{DESCRIPTION}", rule_title=rule_title) }}}
<criteria>
</criteria>
</definition>
</def-group>
Note: rule_id and rule_title are automatically populated during build from the rule.yml.
bash/shared.sh:
ansible/shared.yml:
- name: "{TITLE}"
ansible.builtin.debug:
msg: "Remediation not yet implemented"
tests/correct.pass.sh:
#!/bin/bash
tests/wrong.fail.sh:
#!/bin/bash
Phase 6: Add to Component
Every rule must belong to a component. Components are defined in components/*.yml and map rules to their associated packages and groups.
Step 1: Identify Appropriate Component
Based on the rule's purpose and location, suggest likely components:
ls components/*.yml | sed 's|components/||;s|\.yml||' | sort
Finding the right component: Search existing component files for rules with a similar prefix to identify the likely component:
grep -l '<rule_prefix>' components/*.yml
Step 2: Verify Component Exists
cat components/<component_name>.yml
Step 3: Ask User to Confirm or Select Component
Use AskUserQuestion with options:
- Suggested component (if identified)
- Browse/search for another component
- Create a new component
Step 4a: Add Rule to Existing Component
If user selects an existing component:
-
Read the component file:
cat components/<component_name>.yml
-
Add the rule ID to the rules: list in alphabetical order:
rules:
- existing_rule_1
- existing_rule_2
- $ARGUMENTS
- existing_rule_3
-
If using a template, verify the template is listed in templates: section. If not, add it:
templates:
- existing_template
- {TEMPLATE_NAME}
Step 4b: Create New Component
If no suitable component exists:
-
Ask for component details:
- Component name (lowercase, hyphenated, e.g.,
my-service)
- Associated packages (list)
- Associated groups (from rule hierarchy)
-
Create the component file components/<component_name>.yml:
groups:
- <group_from_rule_hierarchy>
name: <component_name>
packages:
- <package_name>
rules:
- $ARGUMENTS
templates:
- {TEMPLATE_NAME}
-
Verify the new component:
python3 -c "import yaml; yaml.safe_load(open('components/<component_name>.yml'))"
Component File Structure Reference
groups:
- service_name
- service_name_server
name: component-name
packages:
- package-name
- package-name-server
rules:
- rule_id_1
- rule_id_2
templates:
- template_name
Phase 7: Add to Profile(s) or Control File(s)
Most profiles in the project reference control files rather than listing rules directly. When adding a rule to a profile that uses a control file, add the rule to the control file instead.
Step 1: List Available Profiles
Use mcp__content-agent__list_profiles with product=<product> to list all available profiles for each target product.
Fallback: Run ls products/<product>/profiles/*.profile to list profiles.
Step 2: Ask User Which Profile(s)
Use AskUserQuestion to ask which profile(s) to add the rule to. Allow multiple selection.
Step 3: Detect Control File Reference
For each selected profile, read it and check if it references a control file:
grep -E "^\s+-\s+\w+:all" products/<product>/profiles/<profile>.profile
Control file reference patterns:
cis_rhel9:all or cis_rhel9:all:l2_server → Control file: products/rhel9/controls/cis_rhel9.yml
stig_rhel9:all → Control file: products/rhel9/controls/stig_rhel9.yml
hipaa:all → Control file: controls/hipaa.yml (shared across products)
How to find the control file:
- Extract the control ID from the selection (e.g.,
cis_rhel9 from cis_rhel9:all:l2_server)
- Look in
products/<product>/controls/<control_id>.yml first
- If not found, look in
controls/<control_id>.yml
Step 4a: Add to Control File (if profile uses control file)
If the profile references a control file:
-
Read the control file to understand its structure
-
Ask user for control placement:
- For CIS: Ask for the section ID (e.g.,
5.2.15 for SSH settings)
- For STIG: Ask for the STIG ID (e.g.,
RHEL-09-123456)
- Show the control file structure to help user decide
-
Determine the level(s) for the rule:
- CIS:
l1_server, l2_server, l1_workstation, l2_workstation
- STIG:
high, medium, low
-
Add a new control entry to the control file:
For CIS control files:
- id: 5.2.15
title: Ensure SSH MaxAuthTries is set to 4 or less (Automated)
levels:
- l1_server
- l1_workstation
status: automated
rules:
- $ARGUMENTS
For STIG control files:
- id: RHEL-09-123456
levels:
- medium
title: RHEL 9 must limit the number of SSH authentication attempts.
rules:
- $ARGUMENTS
status: automated
-
Insert in the correct position (controls are typically ordered by ID)
Step 4b: Add Directly to Profile (if no control file)
If the profile lists rules directly (no control file reference):
- Read the profile file
- Add the rule ID to the
selections: list
- Maintain alphabetical order if the profile uses it
Control File Locations
Product-specific (preferred for product-specific benchmarks):
products/rhel8/controls/*.yml
products/rhel9/controls/*.yml
products/rhel10/controls/*.yml
Shared (for cross-product policies):
Common Control Files by Profile
| Profile | Control File |
|---|
cis.profile (rhel9) | products/rhel9/controls/cis_rhel9.yml |
stig.profile (rhel9) | products/rhel9/controls/stig_rhel9.yml |
hipaa.profile | controls/hipaa.yml |
pci-dss.profile | controls/pcidss_4.yml |
ospp.profile | controls/ospp.yml |
anssi_bp28_*.profile | controls/anssi.yml |
Reference Security Policies
When adding to CIS or STIG control files, reference the corresponding security policy document to find the correct control ID:
- CIS RHEL 9:
security_policies/CIS_Red_Hat_Enterprise_Linux_9_Benchmark_v2.0.0.md
- STIG RHEL 9:
security_policies/Red Hat Enterprise Linux 9 STIG V2R5 - STIG-A-View.md
Step 5: Update Profile Stability Test Data
IMPORTANT: Whenever a rule is added to a profile (whether directly or via control file), the profile stability test data must also be updated.
Profile stability test data is located in tests/data/profile_stability/<product>/<profile>.profile and contains a sorted list of rule IDs, one per line.
-
Check if stability test file exists:
ls tests/data/profile_stability/<product>/<profile>.profile
-
If the file exists, add the rule ID in alphabetical order:
(cat tests/data/profile_stability/<product>/<profile>.profile; echo "$ARGUMENTS") | sort -u > /tmp/profile_stability_tmp
mv /tmp/profile_stability_tmp tests/data/profile_stability/<product>/<profile>.profile
-
If the file doesn't exist, this may be a new profile or a product without stability tests. Check if the product directory exists:
ls tests/data/profile_stability/<product>/
- If the product directory exists but the profile file doesn't, create it with just the new rule
- If the product directory doesn't exist, skip this step (stability tests may not be set up for this product)
Profile stability test file format:
rule_id_1
rule_id_2
rule_id_3
...
Rules are listed one per line, sorted alphabetically, with no duplicates.
Discover products with stability tests:
ls tests/data/profile_stability/
Phase 8: Verify and Report
-
Verify created files:
ls -la <rule_directory>/
cat <rule_directory>/rule.yml
-
Validate rule YAML using mcp__content-agent__validate_rule_yaml with the content of the generated rule.yml. This validates syntax, structure, and reference formats.
Fallback: Validate against the JSON schema:
python3 -c "
import json, yaml, sys
from jsonschema import validate, ValidationError
schema = json.load(open('shared/schemas/rule.json'))
data = yaml.safe_load(open(sys.argv[1]))
try:
validate(instance=data, schema=schema)
print('Valid')
except ValidationError as e:
print(f'Invalid: {e.message}')
" <path_to_rule.yml>
For control files, use mcp__content-agent__validate_control_file with the control file path.
Fallback: Validate against the control schema:
python3 -c "
import json, yaml, sys
from jsonschema import validate, ValidationError
schema = json.load(open('shared/schemas/control.json'))
data = yaml.safe_load(open(sys.argv[1]))
try:
validate(instance=data, schema=schema)
print('Valid')
except ValidationError as e:
print(f'Invalid: {e.message}')
" <path_to_control.yml>
-
Verify CCE was removed from available list (if CCE was allocated):
grep "<allocated_cce>" shared/references/cce-redhat-avail.txt
-
Verify rule is in component:
grep "$ARGUMENTS" components/<component>.yml
-
Verify profile stability test data (if profile was modified):
grep "$ARGUMENTS" tests/data/profile_stability/<product>/<profile>.profile
-
Report to user:
- List all created files
- Show CCE allocations (and confirm removal from available list)
- Show component file modification
- Show control file or profile modifications
- Show profile stability test data updates
- Explain that
stigid and cis references will be automatically populated from the control file
- Provide next steps:
- For templated rules: "Use
/test-rule $ARGUMENTS to test"
- For non-templated rules: "Complete the OVAL, Bash, Ansible, and test files, then use
/test-rule $ARGUMENTS"
- "Use
/build-product <product> to build and /run-tests to validate"
Important Notes
- Do NOT make test files executable - the test framework handles this
- Use the project's custom Jinja2 delimiters — this project does NOT use standard Jinja2 syntax. The custom delimiters (defined in
ssg/jinja.py) avoid conflicts with YAML/XML curly braces:
- Variables/expressions:
{{{ expr }}} (triple braces), NOT {{ expr }}
- Statements (if/for/set):
{{% stmt %}}, NOT {% stmt %}
- Comments:
{{# comment #}}, NOT {# comment #}
- Examples:
{{{ full_name }}}, {{{ describe_service_enable(service="auditd") }}}, {{% if product in ["rhel9"] %}}, {{% set var="value" %}}
- Check existing similar rules for reference on structure and content
- Templated rules are preferred when a suitable template exists
- Every rule must belong to a component - add to existing component or create new one
- Control files are preferred over direct profile modification - they provide better structure and automatic reference population
- CCE identifiers must be unique - always remove allocated CCEs from
cce-redhat-avail.txt
- Update profile stability test data - when adding to a profile, also update
tests/data/profile_stability/<product>/<profile>.profile
Error Handling
- If rule creation fails, clean up any partially created files
- If CCE allocation fails, do not remove CCEs from the available list
- Validate YAML syntax before writing
- Check for common mistakes:
- Missing required fields
- Invalid template names
- Malformed references
- Duplicate control IDs in control files
- Invalid level names in control files
- Rule not added to any component
- Component file syntax errors
- Profile stability test data not updated