Git 및 CI/CD를 사용한 코드로서의 탐지 파이프라인
2025년에는 오직 보안팀의 35% 용도 전문가의 63%에도 불구하고 체계적으로 코드로 탐지 귀하는 이를 정기적으로 채택하겠다고 선언합니다. 의도와 실천 사이의 이러한 격차 실제 문제를 드러냅니다. 대부분의 조직은 모릅니다. ~처럼 전체 주기를 처리하는 강력한 DaC 파이프라인 구축 생성부터 프로덕션 배포까지 탐지 규칙의 수명.
코드로 감지(DaC) 엔지니어링 관행을 적용합니다 보안 도메인에 대한 소프트웨어 관리: 버전 제어, 코드 검토, 테스트 자동화, CI/CD 파이프라인 및 제어된 배포가 가능합니다. 결과는 하나 새로운 공격 기법 발견까지 걸리는 시간 대폭 단축 품질 규칙을 갖춘 효과적인 탐지 배포 시간이 지남에 따라 측정 가능하고 개선 가능합니다.
이 기사에서 배울 내용
- DaC 저장소의 구조: 디렉터리 레이아웃, 명명 규칙, 스키마
- Git 워크플로: 분기 전략, 커밋 규칙, 풀 요청 검토
- GitHub Actions 파이프라인: 린트, 검증, 테스트, 변환, 배포
- 합성 로그를 사용한 자동 테스트: 긍정적 및 부정적
- API를 통해 Splunk, Elastic 및 Sentinel에 안전하게 배포
- 자동 롤백 및 배포 후 사고 관리
- 품질 지표: 적용 범위, 거짓 긍정 비율, 배포 빈도
- 감지 추적을 위해 JIRA/GitHub 문제와 통합
코드로서의 탐지가 기본이기 때문입니다.
DaC 이전에는 SIEM에서 규칙을 업데이트하는 일반적인 프로세스는 다음과 같습니다. 웹 콘솔에 액세스하여 규칙을 찾고, 수동으로 편집하고, 저장합니다. 그것이 아무것도 깨지지 않기를 바랍니다. 이전 버전 없음, 테스트 없음, 없음 동료 검토, 변경 사항 추적이 불가능합니다. 규칙이 시작되었을 때 새벽 3시에 수천 개의 오탐지를 생성하는 것은 이해할 수 없었습니다. 누가 언제 무엇을 했는지.
DaC는 이 혼란스러운 프로세스를 체계적인 프로세스로 변환합니다.
- 각 규칙은 Git 버전의 텍스트 파일입니다.
- 각 변경 사항은 필수 검토가 포함된 풀 요청을 거칩니다.
- 병합하기 전에 파이프라인은 합성 로그에 대해 자동 테스트를 실행합니다.
- 모든 테스트가 통과한 경우에만 배포가 발생합니다.
- 문제가 발생한 경우 롤백은 간단한 문제입니다.
git revert
DaC 저장소의 구조
잘 구조화된 DaC 저장소는 다른 모든 것이 구축되는 기반입니다. 기업 조직에 권장되는 레이아웃은 다음과 같습니다.
detection-as-code/
├── .github/
│ ├── workflows/
│ │ ├── pr-validation.yml # Validazione su ogni PR
│ │ ├── main-deploy.yml # Deploy su merge in main
│ │ └── nightly-test.yml # Test notturno su staging
│ ├── PULL_REQUEST_TEMPLATE.md
│ └── CODEOWNERS
├── rules/
│ ├── windows/
│ │ ├── credential_access/
│ │ ├── lateral_movement/
│ │ └── persistence/
│ ├── linux/
│ ├── cloud/
│ │ ├── aws/
│ │ ├── azure/
│ │ └── gcp/
│ └── 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
│ ├── test_runner.py
│ ├── convert.py
│ └── deploy_splunk.py
└── Makefile
명명 규칙 및 규칙 체계
저장소 탐색 가능성을 위한 일관되고 필수적인 명명 규칙 수백 가지 규칙이 있습니다. 권장되는 파일 이름 규칙:
# 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
DaC의 분기 전략
# Flusso standard per nuove detection
git checkout -b detection/T1003-lsass-dump-via-procdump
# ... crea/modifica la regola e i test ...
git add rules/windows/credential_access/cred_lsass_procdump.yml
git add tests/fixtures/windows/process_creation/procdump_positive.json
git commit -m "feat(detection): T1003.001 LSASS dump via ProcDump
Adds detection for LSASS memory dump via legitimate ProcDump utility.
ATT&CK: T1003.001 - OS Credential Dumping: LSASS Memory
Severity: high
Test coverage: 3 positive, 2 negative fixtures"
git push origin detection/T1003-lsass-dump-via-procdump
# Flusso hotfix per detection urgenti (attacco in corso)
git checkout -b hotfix/active-incident-lateral-movement-mar2026
git commit -m "hotfix(detection): emergency rule for active APT lateral movement
Active incident response. Incident: INC-2026-0312"
GitHub Actions로 CI/CD 파이프라인 완성
# .github/workflows/pr-validation.yml
name: Detection Rule Validation
on:
pull_request:
paths:
- 'rules/**/*.yml'
- 'tests/**'
jobs:
lint-yaml:
name: YAML Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run yamllint
uses: ibiqlik/action-yamllint@v3
with:
file_or_dir: rules/
config_data: |
extends: default
rules:
line-length:
max: 120
validate-schema:
name: Schema Validation
runs-on: ubuntu-latest
needs: lint-yaml
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:
name: Unit Tests
runs-on: ubuntu-latest
needs: validate-schema
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:
name: Conversion Test
runs-on: ubuntu-latest
needs: unit-tests
strategy:
matrix:
target: [splunk, elasticsearch, microsoft365defender]
steps:
- uses: actions/checkout@v4
- run: |
pip install sigma-cli \
pySigma-backend-splunk \
pySigma-backend-elasticsearch \
pySigma-backend-microsoft365defender
- name: Test conversion to ${{ matrix.target }}
run: |
sigma convert -t ${{ matrix.target }} rules/ \
2>&1 | tee conversion-${{ matrix.target }}.log
# .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 changed rules
run: |
pip install sigma-cli pySigma-backend-splunk
echo "${{ steps.changed-rules.outputs.files }}" | while read rule; do
[ -f "$rule" ] && sigma convert -t splunk -p splunk_windows "$rule" \
>> converted.spl
done
- name: Deploy to Splunk via REST API
env:
SPLUNK_HOST: ${{ secrets.SPLUNK_HOST }}
SPLUNK_TOKEN: ${{ secrets.SPLUNK_TOKEN }}
run: |
python scripts/deploy_splunk.py \
--rules-file converted.spl \
--host "$SPLUNK_HOST" \
--token "$SPLUNK_TOKEN" \
--dry-run false
- name: Notify on failure
if: failure()
uses: actions/github-script@v7
with:
script: |
github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: 'DEPLOY FAILURE: Detection rules deployment failed',
body: 'Run: ' + context.serverUrl + '/' +
context.repo.owner + '/' + context.repo.repo +
'/actions/runs/' + context.runId,
labels: ['incident', 'deployment-failure']
})
탐지 규칙에 대한 테스트 프레임워크
테스트는 DaC의 핵심입니다. 전체 Python 테스트 프레임워크는 다음과 같습니다.
#!/usr/bin/env python3
"""Detection Rule Test Framework."""
import json
import yaml
import pytest
import subprocess
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]:
"""Carica tutti i test case dalla struttura di directory."""
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:
"""Test suite per le detection rules."""
@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)
# Valuta la regola contro l'evento (implementazione omessa per brevita)
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 match.\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 match (false positive).\n"
f"Event: {json.dumps(log_event, indent=2)}"
)
def test_no_duplicate_ids(self):
"""Verifica che non ci siano UUID duplicati nel 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 ID of {seen_ids[rule_id]}"
)
else:
seen_ids[rule_id] = rule_file
assert not duplicates, "Duplicate IDs:\n" + "\n".join(duplicates)
def test_all_rules_have_tests(self):
"""Verifica che ogni regola abbia almeno un test positivo."""
rules_dir = Path("rules")
fixtures_dir = Path("tests/fixtures")
missing = []
for rule_file in rules_dir.rglob("*.yml"):
rel_path = rule_file.relative_to(rules_dir)
pos_dir = fixtures_dir / rel_path.parent / rule_file.stem / "positive"
if not pos_dir.exists() or not list(pos_dir.glob("*.json")):
missing.append(str(rule_file))
if missing:
pytest.fail(
"Rules missing positive test fixtures:\n" +
"\n".join(f" - {r}" for r in missing)
)
테스트 픽스처: 합성 로그의 예
# tests/fixtures/windows/credential_access/
# cred_mimikatz_lsass/positive/mimikatz_direct.json
{
"EventID": 10,
"Channel": "Microsoft-Windows-Sysmon/Operational",
"SourceImage": "C:\\Users\\attacker\\Downloads\\mimikatz.exe",
"TargetImage": "C:\\Windows\\System32\\lsass.exe",
"GrantedAccess": "0x1010",
"UtcTime": "2026-03-09 14:30:00.123"
}
# cred_mimikatz_lsass/negative/windows_defender_scan.json
{
"EventID": 10,
"Channel": "Microsoft-Windows-Sysmon/Operational",
"SourceImage": "C:\\Program Files\\Windows Defender\\MsMpEng.exe",
"TargetImage": "C:\\Windows\\System32\\lsass.exe",
"GrantedAccess": "0x1000",
"UtcTime": "2026-03-09 14:31:00.456"
}
DaC 품질 지표
| 미터법 | 정의 | 목표 |
|---|---|---|
| 탐지 범위 | 안정적인 규칙이 적용되는 % ATT&CK 기술 | > 60% |
| 테스트 범위 | 양성 및 음성 테스트가 포함된 % 규칙 | 100% |
| 거짓양성률 | FP 알림/주별 총 알림 | < 10% |
| 배포 빈도 | 매주 새로운 규칙과 병합 | > 3/주 |
| 감지 시간(TTD) | 기술 게시부터 규칙 배포까지의 시간 | < 48시간(높은 심각도) |
피해야 할 안티패턴
- 테스트 없이 배포: 비상 상황에서도 최소한 한 번의 수동 연기 테스트
- 명확한 비밀: 항상 GitHub Secret을 사용하고 하드코딩하지 마세요.
- 알림이 없는 파이프라인: 조용하고 위험한 배포 실패
- 소유자가 없는 규칙: 각 기술 분야에 CODEOWNERS를 사용하세요.
- 양성 테스트만: 네거티브 픽스처가 없으면 FP를 생성하는지 알 수 없습니다.
결론
코드로서의 탐지는 단순한 도구 변경이 아니라 사고방식의 변화입니다. 탐지는 코드이므로 테스트하고, 버전을 지정하고, 수정해야 합니다. 체계적으로 배포합니다. 성숙한 DaC 파이프라인은 감지 시간을 단축합니다. 규칙의 품질을 높이고 탐지 프로그램을 확장할 수 있습니다. 직원을 비례적으로 늘리지 않고.
시리즈의 다음 기사
다음 글에서는 통합하는 방법을 알아보겠습니다. 마이터 공격&CK DaC 워크플로에서 적용 범위 격차를 자동으로 매핑하고 우선순위를 지정합니다. 실제 위험을 기반으로 한 새로운 탐지.







