The CI/CD pipeline is what makes detection-as-code real — it replaces “hope the rule works” with “the rule passed automated validation and was deployed consistently.” This lesson builds a complete GitHub Actions pipeline for a Sigma detection repository.
The Pipeline Architecture
On: PR opened
│
├─── Job: lint
│ └─ sigma check all changed rules
│
├─── Job: convert (depends on: lint)
│ ├─ sigma convert -t splunk -p sysmon
│ ├─ sigma convert -t elasticsearch -p ecs-windows
│ └─ sigma convert -t microsoft365defender
│
└─── Job: test (depends on: convert)
├─ Run TP tests → assert results ≥ 1
└─ Run FP tests → assert results = 0
On: push to main (merge)
│
├─── Job: convert-all
│ └─ Full conversion for all target backends
│
├─── Job: deploy-dev
│ └─ Push converted rules to dev SIEM
│
├─── Job: deploy-staging (depends on: deploy-dev + integration test pass)
│ └─ Push to staging SIEM (shadow mode)
│
└─── Job: deploy-prod (requires: manual approval)
└─ Push to production SIEM
The GitHub Actions Workflow
PR Checks (.github/workflows/pr-checks.yml)
name: Detection Rule PR Checks
on:
pull_request:
branches: [ main ]
paths:
- 'rules/**'
- 'tests/**'
- 'pipelines/**'
jobs:
lint:
name: Lint Sigma Rules
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install sigma-cli
run: pip install sigma-cli pySigma-backend-splunk pySigma-backend-elasticsearch
- name: Get changed rule files
id: changed-files
uses: tj-actions/changed-files@v41
with:
files: rules/**/*.yml
- name: Lint changed rules
if: steps.changed-files.outputs.any_changed == 'true'
run: |
for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
echo "Checking: $file"
sigma check "$file"
done
convert:
name: Test Sigma Conversion
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: pip install sigma-cli pySigma-backend-splunk pySigma-backend-elasticsearch pySigma-backend-microsoft365defender
- name: Get changed rule files
id: changed-files
uses: tj-actions/changed-files@v41
with:
files: rules/**/*.yml
- name: Convert to all backends
if: steps.changed-files.outputs.any_changed == 'true'
run: |
for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
echo "Converting: $file"
sigma convert -t splunk -p sysmon "$file" > /dev/null
sigma convert -t elasticsearch -p ecs-windows "$file" > /dev/null
sigma convert -t microsoft365defender "$file" > /dev/null
echo " ✓ All backends converted successfully"
done
test:
name: Run Rule Tests
runs-on: ubuntu-latest
needs: convert
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: pip install sigma-cli sigma-testing pySigma-backend-test
- name: Find test files for changed rules
id: changed-files
uses: tj-actions/changed-files@v41
with:
files: rules/**/*.yml
- name: Run tests
if: steps.changed-files.outputs.any_changed == 'true'
run: |
for rule in ${{ steps.changed-files.outputs.all_changed_files }}; do
# Derive test file path from rule path
rule_name=$(basename "$rule" .yml)
test_dir=$(dirname "$rule" | sed 's|rules/|tests/|')
test_file="${test_dir}/${rule_name}.test.yml"
if [ -f "$test_file" ]; then
echo "Testing: $rule → $test_file"
sigma test "$test_file"
else
echo "WARNING: No test file found for $rule"
# Optionally fail if test file is required:
# exit 1
fi
done
Deployment (.github/workflows/deploy.yml)
name: Deploy Detection Rules
on:
push:
branches: [ main ]
paths:
- 'rules/**'
jobs:
convert-all:
name: Convert All Changed Rules
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2 # need previous commit to detect changes
- name: Install dependencies
run: pip install sigma-cli pySigma-backend-splunk pySigma-backend-elasticsearch
- name: Convert changed rules to all targets
run: |
git diff --name-only HEAD~1 -- 'rules/**/*.yml' | while read file; do
sigma convert -t splunk -p sysmon "$file" > \
"converted/splunk/$(basename $file .yml).spl"
sigma convert -t elasticsearch -p ecs-windows "$file" > \
"converted/elastic/$(basename $file .yml).ndjson"
done
deploy-dev:
name: Deploy to Dev SIEM
runs-on: ubuntu-latest
needs: convert-all
environment: dev
steps:
- uses: actions/checkout@v4
- name: Deploy to Splunk Dev
env:
SPLUNK_URL: ${{ secrets.SPLUNK_DEV_URL }}
SPLUNK_TOKEN: ${{ secrets.SPLUNK_DEV_TOKEN }}
run: |
python scripts/deploy_splunk.py \
--url "$SPLUNK_URL" \
--token "$SPLUNK_TOKEN" \
--rules-dir converted/splunk/
deploy-staging:
name: Deploy to Staging (Shadow Mode)
runs-on: ubuntu-latest
needs: deploy-dev
environment: staging # GitHub environment with required reviewers
steps:
- name: Deploy to staging SIEM
# ... similar to dev deploy, but targets staging SIEM
deploy-prod:
name: Deploy to Production
runs-on: ubuntu-latest
needs: deploy-staging
environment:
name: production
# Requires manual approval in GitHub → Settings → Environments
steps:
- name: Deploy to production SIEM
# ... targets production SIEM
Branch Protection Configuration
In GitHub → Settings → Branches → Add protection rule for main:
✓ Require a pull request before merging
✓ Require approvals: 1
✓ Dismiss stale PR approvals when new commits are pushed
✓ Require review from Code Owners
✓ Require status checks to pass before merging
Required status checks:
- lint
- convert
- test
✓ Require branches to be up to date before merging
✓ Restrict who can push to matching branches (detection team leads only)
✗ Allow force pushes (DISABLED)
✗ Allow deletions (DISABLED)
With these settings: no PR can merge without all CI checks passing AND at least one reviewer approving. No one can push directly to main. This technically enforces the detection-as-code policy.
The CODEOWNERS File
Automatically assign reviewers based on which rules changed:
# .github/CODEOWNERS
# Default: require detection-team-leads review for everything
* @detection-team/leads
# Cloud rules require cloud security team review
rules/cloud/aws/** @detection-team/leads @cloud-security
rules/cloud/azure/** @detection-team/leads @cloud-security
# Pipelines changes require senior engineer
pipelines/** @detection-team/senior-engineers
Any PR touching cloud rules automatically requests review from both the detection team lead AND the cloud security team. Pipeline changes (which affect all rule conversions) require senior engineers.
GitHub Actions— GitHub Branch Protection— GitHub