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_ruleswithquery=$ARGUMENTSto check if a rule with this ID already exists. Fallback: UseGlobto 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 underlinux_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_templatesto get all available templates with their descriptions. Fallback: Runls 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_schemawithtemplate_name=<selected_template>to get the full parameter schema, supported languages, and documentation. Fallback: Readshared/templates/<template_name>/template.ymlortemplate.pyfor 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(...) }}}
- Can use macros like
Rationale: Why this rule is important for security
Severity: One of
low,medium,high,unknown. Default value would bemedium.
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 fromcce-redhat-avail.txtto prevent reuse
- Read available CCEs from
- If user doesn't want CCEs now, leave blank (can be added later)
# Read first available CCE head -1 shared/references/cce-redhat-avail.txt # After using a CCE, remove it from the file sed -i '1d' shared/references/cce-redhat-avail.txt- Format:
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)stigidandcisreferences 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]
- Example:
OCIL (optional): OCIL check description
- Often uses macros like
{{{ complete_ocil_entry_sshd_option(...) }}}
- Often uses macros like
Phase 5: Generate Rule
For Templated Rules
Use mcp__content-agent__generate_rule_from_template with:
template_name: The selected template nameparameters: Template variables collected from the userrule_id: $ARGUMENTSproduct: 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: $ARGUMENTStitle: The rule titledescription: The rule descriptionseverity: The severity levelproduct: The target productrationale: 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>
<!-- Add OVAL criteria here -->
</criteria>
</definition>
<!-- Add OVAL tests, objects, and states here -->
</def-group>
Note: rule_id and rule_title are automatically populated during build from the rule.yml.
bash/shared.sh:
# platform = multi_platform_all
# reboot = false
# strategy = configure
# complexity = low
# disruption = low
# Remediation script for $ARGUMENTS
# TODO: Implement remediation
ansible/shared.yml:
# platform = multi_platform_all
# reboot = false
# strategy = configure
# complexity = low
# disruption = low
- name: "{TITLE}"
# TODO: Implement remediation
ansible.builtin.debug:
msg: "Remediation not yet implemented"
tests/correct.pass.sh:
#!/bin/bash
# packages = {REQUIRED_PACKAGES}
# Set up compliant state
# TODO: Configure system to be compliant
tests/wrong.fail.sh:
#!/bin/bash
# packages = {REQUIRED_PACKAGES}
# Set up non-compliant state
# TODO: Configure system to be non-compliant
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:
# List all available 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:
# Find which component contains rules with a similar prefix
grep -l '<rule_prefix>' components/*.yml
Step 2: Verify Component Exists
# Check if suggested 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>.ymlAdd the rule ID to the
rules:list in alphabetical order:rules: - existing_rule_1 - existing_rule_2 - $ARGUMENTS # Add new rule here - existing_rule_3If using a template, verify the template is listed in
templates:section. If not, add it:templates: - existing_template - {TEMPLATE_NAME} # Add if not present
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)
- Component name (lowercase, hyphenated, e.g.,
Create the component file
components/<component_name>.yml:groups: - <group_from_rule_hierarchy> name: <component_name> packages: - <package_name> rules: - $ARGUMENTS templates: - {TEMPLATE_NAME} # If templated ruleVerify the new component:
python3 -c "import yaml; yaml.safe_load(open('components/<component_name>.yml'))"
Component File Structure Reference
groups: # Rule groups this component covers (from guide hierarchy)
- service_name
- service_name_server
name: component-name # Matches filename without .yml
packages: # Packages associated with this component
- package-name
- package-name-server
rules: # Rule IDs belonging to this component (alphabetical)
- rule_id_1
- rule_id_2
templates: # Templates used by rules in this component
- 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:allorcis_rhel9:all:l2_server→ Control file:products/rhel9/controls/cis_rhel9.ymlstig_rhel9:all→ Control file:products/rhel9/controls/stig_rhel9.ymlhipaa: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_rhel9fromcis_rhel9:all:l2_server) - Look in
products/<product>/controls/<control_id>.ymlfirst - 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.15for SSH settings) - For STIG: Ask for the STIG ID (e.g.,
RHEL-09-123456) - Show the control file structure to help user decide
- For CIS: Ask for the section ID (e.g.,
Determine the level(s) for the rule:
- CIS:
l1_server,l2_server,l1_workstation,l2_workstation - STIG:
high,medium,low
- CIS:
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: - $ARGUMENTSFor STIG control files:
- id: RHEL-09-123456 levels: - medium title: RHEL 9 must limit the number of SSH authentication attempts. rules: - $ARGUMENTS status: automatedInsert 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/*.ymlproducts/rhel9/controls/*.ymlproducts/rhel10/controls/*.yml
Shared (for cross-product policies):
controls/*.yml
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>.profileIf the file exists, add the rule ID in alphabetical order:
# Read current file, add new rule, sort, and write back (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>.profileIf 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.ymlValidate rule YAML using
mcp__content-agent__validate_rule_yamlwith 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_filewith 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 # should return nothingVerify rule is in component:
grep "$ARGUMENTS" components/<component>.ymlVerify profile stability test data (if profile was modified):
grep "$ARGUMENTS" tests/data/profile_stability/<product>/<profile>.profileReport 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
stigidandcisreferences will be automatically populated from the control file - Provide next steps:
- For templated rules: "Use
/test-rule $ARGUMENTSto 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-teststo validate"
- For templated rules: "Use
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" %}}
- Variables/expressions:
- 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