Skip to main content

02 - Detection-as-Code Pipeline with Git & CI/CD

Detection-as-Code Pipeline con Git e CI/CD Nel 2025, solo il 35% dei team di sicurezza utilizza Detection-as-Code in modo sistematico, nonostante il 63%...

What do you think about this article?

At a Glance

Detection-as-Code Pipeline con Git e CI/CD Nel 2025, solo il 35% dei team di sicurezza utilizza Detection-as-Code in modo sistematico, nonostante il 63%...

Detection-as-Code Pipeline with Git and CI/CD

In 2025, only 35% of security teams use Detection-as-Code systematically, despite 63% of professionals saying they would like to adopt it regularly. This gap between intention and practice reveals a real problem: most organizations do not know how to build a robust DaC pipeline that manages the entire lifecycle of a detection rule, from creation to production deployment.

Detection-as-Code (DaC) applies software engineering practices to the security domain: version control, code review, automated testing, CI/CD pipelines, and controlled deployment. The result is a drastic reduction in the time between discovering a new attack technique and deploying an effective detection, with rule quality that is measurable and improvable over time.

What You Will Learn

  • DaC repository structure: directory layout, naming convention, schema
  • Git workflow: branching strategy, commit convention, pull request review
  • GitHub Actions pipeline: lint, validate, test, convert, deploy
  • Automated testing with synthetic logs: positive and negative fixtures
  • Secure deployment to Splunk, Elastic, and Sentinel via API
  • Automatic rollback and incident management post-deploy
  • Quality metrics: coverage, false positive rate, deployment frequency

Why Detection-as-Code is Fundamental

Before DaC, the typical process for updating a rule in a SIEM was: access the web console, find the rule, manually modify it, save it, and hope nothing breaks. No previous version, no tests, no peer review, no change traceability. When a rule started generating thousands of false positives at 3 AM, it was impossible to know who did what and when. DaC transforms this chaotic process into something systematic:

  • Every rule is a text file versioned in Git
  • Every change goes through a pull request with mandatory review
  • Before merging, the pipeline runs automated tests on synthetic logs
  • Deployment only happens if all tests pass
  • If problems arise, rollback is a simple git revert

DaC Repository Structure

detection-as-code/
├── .github/
│   ├── workflows/
│   │   ├── pr-validation.yml      # Validate on every PR
│   │   ├── main-deploy.yml        # Deploy on merge to main
│   │   └── nightly-test.yml       # Nightly test on staging
│   ├── PULL_REQUEST_TEMPLATE.md
│   └── CODEOWNERS
├── rules/
│   ├── windows/
│   │   ├── credential_access/
│   │   ├── lateral_movement/
│   │   └── persistence/
│   ├── linux/
│   ├── cloud/
│   │   ├── aws/
│   │   └── azure/
│   └── network/
├── tests/
│   ├── fixtures/
│   │   └── windows/credential_access/
│   │       └── cred_mimikatz_lsass/
│   │           ├── positive/
│   │           └── negative/
│   └── unit/test_sigma_rules.py
├── pipelines/
│   ├── splunk_enterprise.yml
│   └── elastic_ecs.yml
├── scripts/
│   ├── validate.py
│   ├── convert.py
│   └── deploy_splunk.py
└── Makefile

Naming Convention

# Pattern: <tactic_prefix>_<technique_name>_<context>.yml
cred_mimikatz_lsass.yml           # credential_access
lat_psexec_remote_execution.yml   # lateral_movement
per_scheduled_task_creation.yml   # persistence
def_timestomp_modification.yml    # defense_evasion
exe_powershell_encoded.yml        # execution
exf_dns_tunneling.yml             # exfiltration
c2_http_beaconing.yml             # command_and_control

Complete GitHub Actions CI/CD Pipeline

# .github/workflows/pr-validation.yml
name: Detection Rule Validation

on:
  pull_request:
    paths:
      - 'rules/**/*.yml'
      - 'tests/**'

jobs:
  validate-schema:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - run: pip install pyyaml jsonschema sigma-cli

      - name: Validate Sigma schemas
        run: python scripts/validate.py ./rules --schema schemas/sigma_rule_schema.json

      - name: Check UUID uniqueness
        run: python scripts/check_uuid_uniqueness.py ./rules

  unit-tests:
    needs: validate-schema
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - run: pip install pytest pyyaml sigma-cli pySigma-backend-splunk

      - name: Run detection tests
        run: pytest tests/unit/ -v --tb=short --junit-xml=test-results.xml

  convert-validate:
    needs: unit-tests
    runs-on: ubuntu-latest
    strategy:
      matrix:
        target: [splunk, elasticsearch, microsoft365defender]
    steps:
      - uses: actions/checkout@v4
      - run: pip install sigma-cli pySigma-backend-splunk pySigma-backend-elasticsearch

      - name: Test conversion to ${{ matrix.target }}
        run: sigma convert -t ${{ matrix.target }} rules/
# .github/workflows/main-deploy.yml
name: Deploy Detection Rules to Production

on:
  push:
    branches: [main]
    paths: ['rules/**/*.yml']

jobs:
  deploy-splunk:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2

      - name: Identify changed rules
        id: changed-rules
        run: |
          CHANGED=$(git diff --name-only HEAD~1 HEAD -- 'rules/**/*.yml')
          echo "files=$CHANGED" >> $GITHUB_OUTPUT

      - name: Convert and deploy
        env:
          SPLUNK_HOST: ${{ secrets.SPLUNK_HOST }}
          SPLUNK_TOKEN: ${{ secrets.SPLUNK_TOKEN }}
        run: |
          pip install sigma-cli pySigma-backend-splunk requests
          echo "${{ steps.changed-rules.outputs.files }}" | while read rule; do
            [ -f "$rule" ] && sigma convert -t splunk -p splunk_windows "$rule" \
              >> converted.spl
          done
          python scripts/deploy_splunk.py \
            --rules-file converted.spl \
            --host "$SPLUNK_HOST" \
            --token "$SPLUNK_TOKEN" \
            --dry-run false

Python Testing Framework for Detection Rules

#!/usr/bin/env python3
"""Detection Rule Test Framework."""
import json
import yaml
import pytest
from pathlib import Path
from typing import NamedTuple

class TestCase(NamedTuple):
    rule_path: Path
    fixture_path: Path
    should_match: bool

def load_test_cases() -> list[TestCase]:
    """Load all test cases from the directory structure."""
    test_cases = []
    rules_dir = Path("rules")
    fixtures_dir = Path("tests/fixtures")

    for rule_file in rules_dir.rglob("*.yml"):
        rel_path = rule_file.relative_to(rules_dir)
        fixture_dir = fixtures_dir / rel_path.parent / rule_file.stem

        if fixture_dir.exists():
            for pos_file in (fixture_dir / "positive").glob("*.json"):
                test_cases.append(TestCase(rule_file, pos_file, True))
            for neg_file in (fixture_dir / "negative").glob("*.json"):
                test_cases.append(TestCase(rule_file, neg_file, False))

    return test_cases

class TestDetectionRules:
    @pytest.mark.parametrize("test_case", load_test_cases())
    def test_rule_against_fixture(self, test_case: TestCase):
        rule_path, fixture_path, should_match = test_case

        with open(fixture_path) as f:
            log_event = json.load(f)

        matched = evaluate_sigma_rule(rule_path, log_event)

        if should_match:
            assert matched, (
                f"Rule {rule_path.name} SHOULD match {fixture_path.name} "
                f"but did NOT.\nEvent: {json.dumps(log_event, indent=2)}"
            )
        else:
            assert not matched, (
                f"Rule {rule_path.name} should NOT match {fixture_path.name} "
                f"but DID (false positive).\n"
                f"Event: {json.dumps(log_event, indent=2)}"
            )

    def test_no_duplicate_ids(self):
        """Ensure no duplicate UUIDs exist in the repository."""
        seen_ids = {}
        duplicates = []

        for rule_file in Path("rules").rglob("*.yml"):
            with open(rule_file) as f:
                rule = yaml.safe_load(f)
            rule_id = rule.get('id', '')

            if rule_id in seen_ids:
                duplicates.append(f"{rule_file} duplicates {seen_ids[rule_id]}")
            else:
                seen_ids[rule_id] = rule_file

        assert not duplicates, "Duplicate IDs:\n" + "\n".join(duplicates)

DaC Quality Metrics

Metric Definition Target
Detection Coverage % ATT&CK techniques with stable rule > 60%
Test Coverage % rules with positive + negative tests 100%
False Positive Rate FP alerts / total alerts per week < 10%
Deployment Frequency Merges with new/updated rules per week > 3/week
Time to Detection (TTD) Time from technique published to rule deployed < 48h for high severity

Anti-Patterns to Avoid

  • Deploy without tests: even in emergencies, run at least one smoke test
  • Hardcoded secrets: always use GitHub Secrets, never hardcoded credentials
  • Silent pipeline failures: a failed deploy with no notification is dangerous
  • Rules without owners: use CODEOWNERS to assign technical responsibility
  • Only positive tests: without negative fixtures you cannot detect false positives

Conclusions

Detection-as-Code is not just a tool change, it is a mindset change: detections are code, and as such must be tested, versioned, reviewed, and deployed systematically. A mature DaC pipeline reduces Time-to-Detection, improves rule quality, and allows scaling the detection program without proportionally increasing headcount.

Next Article in the Series

In the next article we will see how to integrate MITRE ATT&CK into the DaC workflow to automatically map coverage gaps and prioritize new detections based on real risk.

Frequently asked questions

What percentage of security teams use Detection-as-Code systematically?

Only 35% of security teams use Detection-as-Code systematically.

What is the main problem that prevents organizations from adopting a robust Detection-as-Code pipeline?

Most organizations do not know how to build a robust DaC pipeline that manages the entire lifecycle of a detection rule, from creation to production deployment.

How does Detection-as-Code reduce the time between discovering a new attack technique and deploying an effective detection?

The result is a drastic reduction in the time between discovering a new attack technique and deploying an effective detection.

What is the main benefit of adopting a mature Detection-as-Code pipeline?

A mature DaC pipeline reduces Time-to-Detection, improves rule quality, and allows scaling the detection program without proportionally increasing headcount.

Sources and further reading

Try These Tools

Free developer tools related to this topic.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects