테스트 감지 규칙: 보안 논리에 대한 단위 테스트
테스트 없이 애플리케이션 코드를 작성하면 나쁜 개발자로 간주됩니다. 테스트 없이 탐지 규칙을 작성하면... 정상으로 간주됩니다. 이러한 불일치 소프트웨어 엔지니어링과 보안 엔지니어링 사이의 문화와 그 이유 중 하나 탐지 시 오탐지율이 여전히 너무 높음: 테스트되지 않은 규칙 하루에 수백만 건의 이벤트를 프로덕션 환경에 배포합니다.
업계는 보다 엄격한 접근 방식으로 빠르게 수렴하고 있습니다. 스플렁크에 따르면, 2025년에는 보안 전문가의 63%가 테스트에 코드로서의 탐지를 사용하고 싶어합니다. 체계적이지만 실제로는 35%만이 그렇게 합니다. 격차와 기회: 누가 구현하는가 탐지 규칙에 대한 단위 테스트는 더 정확한 규칙을 얻고 오탐률을 줄입니다. 보다 지속 가능한 유지 관리 프로세스.
이 문서에서는 Sigma 탐지 규칙에 대한 완전한 단위 테스트 프레임워크를 구축합니다. 합성 로그 생성부터 pytest를 통한 자동 테스트, 커버리지 분석까지 CI/CD 파이프라인으로의 통합까지 감지 격차를 식별합니다.
무엇을 배울 것인가
- 탐지 규칙에 적용되는 단위 테스트 원칙
- sigma-test: Sigma 규칙을 테스트하기 위한 전용 프레임워크
- 참양성 및 위양성 테스트를 위한 합성 로그 생성
- 탐지 규칙을 위한 사용자 정의 pytest 프레임워크
- ATT&CK 탐지 격차를 식별하기 위한 커버리지 분석
- CI/CD 통합: 배포 전 품질 게이트
탐지 규칙에 테스트가 필요한 이유
탐지 규칙 및 코드. 입력(로그 이벤트), 논리(일치 조건) 및 출력이 있습니다. (경고). 다른 코드와 마찬가지로 잘못된 논리, 잘못된 필드, 조건 등 버그가 있을 수 있습니다. 너무 넓거나 너무 좁습니다. 하지만 애플리케이션 코드와 달리 버그 탐지는 규칙에는 천천히 나타나는 결과가 있습니다. 잘못된 긍정이 너무 많으면 경고가 발생합니다. 피로, 거짓 부정(false negative)으로 인해 공격자가 눈에 띄지 않게 됩니다.
탐지 규칙에 필요한 테스트 유형은 다음과 같습니다.
- 참양성 테스트: 예상되는 악의적인 이벤트가 규칙을 트리거해야 합니다.
- 거짓 양성 테스트: 일반적인 적법한 이벤트가 트리거되어서는 안 됩니다.
- 엣지 케이스 테스트: 악의적인 행동의 변종(다른 인코딩, 선택적 매개변수)
- 회귀 테스트: 변경 사항으로 인해 기존 감지가 중단되지 않도록 보장합니다.
- 성능 테스트: 규칙이 SIEM의 성능에 영향을 미치지 않는지 확인합니다.
sigma-test: 전용 프레임워크
시그마 테스트 (github.com/bradleyjkemp/sigma-test) 및 전문 도구 테스트 이벤트를 직접 지정할 수 있는 시그마 규칙 테스트용 규칙의 YAML 파일에서 YAML 주석으로. 이 접근 방식은 테스트를 유지합니다. 규칙을 적용하여 유지 관리를 용이하게 합니다.
# Sigma Rule con test integrati (formato sigma-test)
# File: rules/windows/t1059_001_powershell_encoded.yml
title: PowerShell Encoded Command Execution
id: 5b4f6d89-1234-4321-ab12-fedcba987654
status: stable
description: >
Rileva esecuzione PowerShell con parametri di encoding, frequentemente
usati da malware per offuscare payload.
references:
- https://attack.mitre.org/techniques/T1059/001/
author: Detection Team
date: 2025-01-15
tags:
- attack.execution
- attack.t1059.001
logsource:
category: process_creation
product: windows
detection:
selection:
Image|endswith:
- '\powershell.exe'
- '\pwsh.exe'
CommandLine|contains:
- ' -enc '
- ' -EncodedCommand '
- ' -ec '
condition: selection
falsepositives:
- Software legittimo enterprise che usa PowerShell con encoding
- Script di deployment automatizzati
level: medium
# Test cases (formato sigma-test)
tests:
- name: "TP: PowerShell con -EncodedCommand"
should_match: true
event:
Image: 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe'
CommandLine: 'powershell.exe -EncodedCommand SQBFAFgAKABOAGUAdAAgAC4AIAAuACkA'
ParentImage: 'C:\Windows\System32\cmd.exe'
- name: "TP: PowerShell con -enc (shorthand)"
should_match: true
event:
Image: 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe'
CommandLine: 'powershell.exe -enc SQBFAFgA'
ParentImage: 'C:\Windows\Explorer.exe'
- name: "TP: pwsh (PowerShell Core) con encoded"
should_match: true
event:
Image: 'C:\Program Files\PowerShell\7\pwsh.exe'
CommandLine: 'pwsh -EncodedCommand SQBFAFgA'
ParentImage: 'C:\Windows\System32\services.exe'
- name: "FP: PowerShell normale senza encoding"
should_match: false
event:
Image: 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe'
CommandLine: 'powershell.exe -ExecutionPolicy Bypass -File C:\scripts\deploy.ps1'
ParentImage: 'C:\Windows\System32\svchost.exe'
- name: "FP: PowerShell con parametro simile ma non encoding"
should_match: false
event:
Image: 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe'
CommandLine: 'powershell.exe Get-Content C:\scripts\encrypted-backup.zip'
ParentImage: 'C:\Windows\System32\svchost.exe'
이러한 테스트를 수행하기 위해 sigma-test는 테스트 이벤트를 규칙 로직과 비교합니다.
그리고 일치가 다음과 일치하는지 확인합니다. should_match:
# Esecuzione sigma-test
# Installa: go install github.com/bradleyjkemp/sigma-test@latest
# Test singola regola
sigma-test rules/windows/t1059_001_powershell_encoded.yml
# Test di tutte le regole in una directory
sigma-test rules/windows/
# Output example:
# PASS rules/windows/t1059_001_powershell_encoded.yml
# TP: PowerShell con -EncodedCommand ... PASS
# TP: PowerShell con -enc (shorthand) ... PASS
# TP: pwsh (PowerShell Core) con encoded ... PASS
# FP: PowerShell normale senza encoding ... PASS
# FP: PowerShell con parametro simile ... PASS
고급 테스트를 위한 Pytest 프레임워크
보다 복잡한 테스트(멀티 플랫폼, 실제 SIEM 테스트, 성능 벤치마크)의 경우 pytest는 뛰어난 유연성을 제공합니다. Sigma의 공식 Python 라이브러리인 pySigma는 이미 pytest를 백엔드의 테스트 프레임워크로 사용하고 있습니다.
# Framework pytest per Detection Rules
# File: tests/test_detection_rules.py
import pytest
import yaml
from pathlib import Path
from sigma.rule import SigmaRule
from sigma.backends.splunk import SplunkBackend
from sigma.backends.elasticsearch import ElasticsearchQueryStringBackend
import re
RULES_DIR = Path("rules")
TESTS_DIR = Path("tests/test_data")
def load_all_rules() -> list[tuple[str, str]]:
"""Carica tutte le regole Sigma dalla directory rules/."""
rules = []
for rule_file in RULES_DIR.glob("**/*.yml"):
content = rule_file.read_text()
rules.append((str(rule_file), content))
return rules
class SigmaRuleSimulator:
"""Simula il matching di una regola Sigma su eventi."""
def __init__(self, rule_yaml: str):
self.rule_dict = yaml.safe_load(rule_yaml)
self.detection = self.rule_dict.get('detection', {})
def matches(self, event: dict) -> bool:
"""Verifica se l'evento matcha la regola (simulazione semplificata)."""
condition = self.detection.get('condition', '')
selectors = {k: v for k, v in self.detection.items() if k != 'condition'}
# Valuta ogni selettore
selector_results = {}
for selector_name, criteria in selectors.items():
if selector_name.startswith('filter'):
selector_results[selector_name] = self._eval_criteria(event, criteria)
else:
selector_results[selector_name] = self._eval_criteria(event, criteria)
# Valuta la condition
return self._eval_condition(condition, selector_results)
def _eval_criteria(self, event: dict, criteria) -> bool:
"""Valuta un selettore contro l'evento."""
if not isinstance(criteria, dict):
return False
for field, values in criteria.items():
# Estrai nome campo e modifier
parts = field.split('|')
field_name = parts[0]
modifier = parts[1] if len(parts) > 1 else 'exact'
event_value = str(event.get(field_name, ''))
# Controlla lista di valori (OR implicito)
if isinstance(values, list):
if not any(self._apply_modifier(event_value, str(v), modifier)
for v in values):
return False
else:
if not self._apply_modifier(event_value, str(values), modifier):
return False
return True
def _apply_modifier(self, event_val: str,
pattern: str, modifier: str) -> bool:
"""Applica il modifier Sigma."""
ev = event_val.lower()
pat = pattern.lower().replace('*', '')
if modifier == 'contains':
return pat in ev
elif modifier == 'endswith':
return ev.endswith(pat)
elif modifier == 'startswith':
return ev.startswith(pat)
elif modifier == 're':
return bool(re.search(pattern, event_val, re.IGNORECASE))
else: # exact
return ev == pat
def _eval_condition(self, condition: str,
results: dict[str, bool]) -> bool:
"""Valuta la condition Sigma (logica base)."""
# Gestisce condizioni semplici comuni
condition = condition.strip()
# "selection" semplice
if condition in results:
return results[condition]
# "selection and not filter"
if ' and not ' in condition:
parts = condition.split(' and not ')
left = self._eval_condition(parts[0].strip(), results)
right = self._eval_condition(parts[1].strip(), results)
return left and not right
# "selection or selection2"
if ' or ' in condition:
parts = condition.split(' or ')
return any(self._eval_condition(p.strip(), results) for p in parts)
# "selection and selection2"
if ' and ' in condition:
parts = condition.split(' and ')
return all(self._eval_condition(p.strip(), results) for p in parts)
# "not selection"
if condition.startswith('not '):
inner = condition[4:].strip()
return not self._eval_condition(inner, results)
return results.get(condition, False)
# ===== TEST CLASSES =====
class TestSigmaRuleSyntax:
"""Test sintattici: tutte le regole devono essere YAML valido."""
@pytest.mark.parametrize("rule_path,rule_content", load_all_rules())
def test_valid_yaml(self, rule_path: str, rule_content: str):
"""Ogni regola deve essere YAML valido e parsabile."""
try:
rule_dict = yaml.safe_load(rule_content)
assert rule_dict is not None, f"YAML vuoto: {rule_path}"
except yaml.YAMLError as e:
pytest.fail(f"YAML invalido in {rule_path}: {e}")
@pytest.mark.parametrize("rule_path,rule_content", load_all_rules())
def test_required_fields(self, rule_path: str, rule_content: str):
"""Ogni regola deve avere i campi obbligatori."""
rule_dict = yaml.safe_load(rule_content)
required = ['title', 'description', 'logsource', 'detection']
for field in required:
assert field in rule_dict, \
f"Campo '{field}' mancante in {rule_path}"
@pytest.mark.parametrize("rule_path,rule_content", load_all_rules())
def test_detection_has_condition(self, rule_path: str, rule_content: str):
"""Ogni regola deve avere una 'condition' in detection."""
rule_dict = yaml.safe_load(rule_content)
detection = rule_dict.get('detection', {})
assert 'condition' in detection, \
f"'condition' mancante in detection di {rule_path}"
@pytest.mark.parametrize("rule_path,rule_content", load_all_rules())
def test_valid_level(self, rule_path: str, rule_content: str):
"""Il level deve essere uno dei valori standard."""
rule_dict = yaml.safe_load(rule_content)
valid_levels = {'informational', 'low', 'medium', 'high', 'critical'}
level = rule_dict.get('level', '')
if level:
assert level in valid_levels, \
f"Level '{level}' non valido in {rule_path}"
@pytest.mark.parametrize("rule_path,rule_content", load_all_rules())
def test_pysigma_parseable(self, rule_path: str, rule_content: str):
"""Ogni regola deve essere parsabile da pySigma."""
from sigma.exceptions import SigmaError
try:
SigmaRule.from_yaml(rule_content)
except SigmaError as e:
pytest.fail(f"pySigma non riesce a parsare {rule_path}: {e}")
테스트를 위한 합성 로그 생성
참양성 및 거짓양성 테스트에는 현실적인 로그 이벤트가 필요합니다. 세대 테스트 매뉴얼이 오래되고 지루하며 불완전합니다. 자동화된 생성기가 이벤트를 생성합니다. 규칙의 모든 변형을 체계적으로 포괄합니다.
# Generatore di Log Sintetici
# File: tests/log_generator.py
from dataclasses import dataclass
from datetime import datetime, timedelta
import random
import string
import uuid
@dataclass
class WindowsProcessEvent:
"""Evento di process_creation Windows (Sysmon Event ID 1)."""
EventID: str = "1"
ComputerName: str = "WORKSTATION01"
User: str = "DOMAIN\\user"
Image: str = "C:\\Windows\\System32\\cmd.exe"
CommandLine: str = "cmd.exe"
ParentImage: str = "C:\\Windows\\Explorer.exe"
ParentCommandLine: str = "explorer.exe"
ProcessId: str = "1234"
ParentProcessId: str = "5678"
MD5: str = ""
SHA256: str = ""
Hashes: str = ""
UtcTime: str = ""
def __post_init__(self):
if not self.UtcTime:
self.UtcTime = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S.%f")
if not self.MD5:
self.MD5 = ''.join(random.choices(string.hexdigits, k=32)).upper()
def to_dict(self) -> dict:
return {k: v for k, v in self.__dict__.items()}
class SyntheticLogGenerator:
"""Genera log sintetici per scenari di testing specifici."""
# Template per tecniche ATT&CK comuni
TEMPLATES = {
'T1059.001_encoded': [
# True Positives
{
'should_match': True,
'name': 'PS encoded via cmd',
'event': WindowsProcessEvent(
Image='C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe',
CommandLine='powershell.exe -EncodedCommand SQBFAFgAKABOAGUAdAAgAC4AIAAuACkA',
ParentImage='C:\\Windows\\System32\\cmd.exe'
)
},
{
'should_match': True,
'name': 'PS enc shorthand',
'event': WindowsProcessEvent(
Image='C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe',
CommandLine='powershell -ec SQBFAFgA',
ParentImage='C:\\Windows\\Explorer.exe'
)
},
# False Positives
{
'should_match': False,
'name': 'PS script normale',
'event': WindowsProcessEvent(
Image='C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe',
CommandLine='powershell.exe -ExecutionPolicy Bypass -File deploy.ps1',
ParentImage='C:\\Windows\\System32\\svchost.exe'
)
},
{
'should_match': False,
'name': 'PS word encrypted (FP trap)',
'event': WindowsProcessEvent(
Image='C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe',
CommandLine='powershell.exe Get-Content C:\\backup\\encrypted.zip',
ParentImage='C:\\Windows\\System32\\TaskScheduler.exe'
)
},
],
'T1003.001_lsass_dump': [
{
'should_match': True,
'name': 'Procdump lsass',
'event': WindowsProcessEvent(
Image='C:\\Tools\\procdump.exe',
CommandLine='procdump.exe -ma lsass.exe lsass_dump.dmp',
ParentImage='C:\\Windows\\System32\\cmd.exe'
)
},
{
'should_match': True,
'name': 'Task Manager lsass dump',
'event': WindowsProcessEvent(
Image='C:\\Windows\\System32\\taskmgr.exe',
CommandLine='taskmgr.exe',
ParentImage='C:\\Windows\\Explorer.exe'
)
},
{
'should_match': False,
'name': 'Normal lsass activity',
'event': WindowsProcessEvent(
Image='C:\\Windows\\System32\\lsass.exe',
CommandLine='lsass.exe',
ParentImage='C:\\Windows\\System32\\wininit.exe'
)
},
],
'T1053.005_scheduled_task': [
{
'should_match': True,
'name': 'schtasks create con cmd',
'event': WindowsProcessEvent(
Image='C:\\Windows\\System32\\schtasks.exe',
CommandLine='schtasks.exe /create /tn "Windows Update" /tr "cmd.exe /c evil.exe" /sc daily',
ParentImage='C:\\Windows\\System32\\cmd.exe'
)
},
{
'should_match': False,
'name': 'schtasks query legittimo',
'event': WindowsProcessEvent(
Image='C:\\Windows\\System32\\schtasks.exe',
CommandLine='schtasks.exe /query /fo LIST',
ParentImage='C:\\Windows\\System32\\svchost.exe'
)
},
]
}
def get_test_cases(self, technique_id: str) -> list[dict]:
"""Restituisce i test case per una tecnica ATT&CK."""
key = technique_id.replace('.', '_').replace('T', 'T', 1)
# Cerca per prefix (es. 'T1059_001')
for template_key, cases in self.TEMPLATES.items():
if template_key.startswith(key.replace('T', 'T', 1)):
return [{
'should_match': c['should_match'],
'name': c['name'],
'event': c['event'].to_dict()
} for c in cases]
return []
def generate_random_events(self, count: int = 100) -> list[dict]:
"""Genera eventi casuali per stress testing (tutti FP)."""
events = []
legitimate_processes = [
'C:\\Windows\\System32\\cmd.exe',
'C:\\Windows\\System32\\svchost.exe',
'C:\\Program Files\\Microsoft Office\\root\\Office16\\EXCEL.EXE',
'C:\\Windows\\System32\\notepad.exe',
'C:\\Windows\\explorer.exe',
]
for _ in range(count):
events.append(WindowsProcessEvent(
Image=random.choice(legitimate_processes),
CommandLine=f"process.exe {''.join(random.choices(string.ascii_lowercase, k=20))}",
ParentImage=random.choice(legitimate_processes),
ComputerName=f"WS-{random.randint(1000, 9999)}"
).to_dict())
return events
pytest로 테스트 완료
테스트 프레임워크, 규칙 시뮬레이터 및 로그 생성기를 결합하여 저장소의 모든 규칙을 체계적으로 다루는 매개변수화된 테스트입니다.
# Test completi con pytest
# File: tests/test_rule_logic.py
class TestRuleLogicWithSyntheticLogs:
"""Test della logica di detection con log sintetici."""
@pytest.fixture
def generator(self) -> SyntheticLogGenerator:
return SyntheticLogGenerator()
def test_powershell_encoded_true_positives(self, generator):
"""Verifica che tutti i TP vengano rilevati."""
rule_content = Path(
"rules/windows/t1059_001_powershell_encoded.yml"
).read_text()
simulator = SigmaRuleSimulator(rule_content)
test_cases = generator.get_test_cases('T1059.001')
tp_cases = [tc for tc in test_cases if tc['should_match']]
assert len(tp_cases) > 0, "Nessun test case TP trovato"
for tc in tp_cases:
result = simulator.matches(tc['event'])
assert result, \
f"FALSE NEGATIVE: '{tc['name']}' non ha triggerato la regola\n" \
f"Event: {tc['event']}"
def test_powershell_encoded_false_positives(self, generator):
"""Verifica che gli eventi legittimi NON vengano rilevati."""
rule_content = Path(
"rules/windows/t1059_001_powershell_encoded.yml"
).read_text()
simulator = SigmaRuleSimulator(rule_content)
test_cases = generator.get_test_cases('T1059.001')
fp_cases = [tc for tc in test_cases if not tc['should_match']]
for tc in fp_cases:
result = simulator.matches(tc['event'])
assert not result, \
f"FALSE POSITIVE: '{tc['name']}' ha triggerato la regola inaspettatamente\n" \
f"Event: {tc['event']}"
def test_stress_no_false_positives(self, generator):
"""Stress test: 100 eventi casuali non devono triggherare."""
rule_content = Path(
"rules/windows/t1059_001_powershell_encoded.yml"
).read_text()
simulator = SigmaRuleSimulator(rule_content)
random_events = generator.generate_random_events(100)
fp_count = sum(1 for ev in random_events if simulator.matches(ev))
# Accettiamo max 2% di false positive su eventi casuali
fp_rate = fp_count / len(random_events)
assert fp_rate <= 0.02, \
f"Tasso FP troppo alto: {fp_rate:.1%} ({fp_count}/{len(random_events)})"
class TestRuleCoverage:
"""Test di coverage: ogni tecnica ATT&CK deve avere almeno una regola."""
CRITICAL_TECHNIQUES = [
'T1059.001', # PowerShell
'T1003.001', # LSASS Dump
'T1055', # Process Injection
'T1053.005', # Scheduled Task
'T1078', # Valid Accounts
'T1021.002', # SMB/Windows Admin Shares
'T1562.001', # Disable Security Tools
'T1070.004', # File Deletion
]
def test_critical_techniques_have_rules(self):
"""Verifica che tutte le tecniche critiche abbiano almeno una regola."""
# Carica tutti i tag dalle regole
covered_techniques = set()
for rule_file in RULES_DIR.glob("**/*.yml"):
content = yaml.safe_load(rule_file.read_text())
tags = content.get('tags', [])
for tag in tags:
if tag.startswith('attack.t'):
# Converti tag.t1059.001 -> T1059.001
technique = tag.replace('attack.', '').upper()
covered_techniques.add(technique)
uncovered = [t for t in self.CRITICAL_TECHNIQUES
if t not in covered_techniques]
assert not uncovered, \
f"Tecniche critiche senza copertura: {uncovered}"
def test_coverage_report(self):
"""Genera report di coverage ATT&CK (informativo, non fail)."""
covered = set()
for rule_file in RULES_DIR.glob("**/*.yml"):
content = yaml.safe_load(rule_file.read_text())
for tag in content.get('tags', []):
if tag.startswith('attack.t'):
covered.add(tag.replace('attack.', '').upper())
print(f"\n=== ATT&CK Coverage Report ===")
print(f"Tecniche coperte: {len(covered)}")
print(f"Tecniche critiche coperte: "
f"{len([t for t in self.CRITICAL_TECHNIQUES if t in covered])}"
f"/{len(self.CRITICAL_TECHNIQUES)}")
print("Dettaglio critico:")
for tech in self.CRITICAL_TECHNIQUES:
status = "OK" if tech in covered else "MANCANTE"
print(f" {tech}: {status}")
CI/CD 통합: 품질 게이트
테스트는 규칙 이전에 각 Pull Request에서 자동으로 실행되어야 합니다. 기본 저장소에 병합되었습니다. 실패한 게이트는 배포를 방해합니다. 생산 규칙이 잘못되었습니다.
# GitHub Actions CI/CD per Detection Rules
# File: .github/workflows/test-detection-rules.yml
"""
name: Detection Rules CI
on:
pull_request:
paths:
- 'rules/**'
- 'tests/**'
push:
branches: [main]
jobs:
syntax-validation:
name: Syntax Validation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: pip
- name: Install dependencies
run: |
pip install pySigma pySigma-backend-splunk pyyaml pytest pytest-cov
- name: Run syntax tests
run: pytest tests/test_detection_rules.py::TestSigmaRuleSyntax -v
logic-testing:
name: Logic Testing
runs-on: ubuntu-latest
needs: syntax-validation
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: pip
- name: Install dependencies
run: pip install pySigma pyyaml pytest pytest-cov
- name: Run logic tests with coverage
run: |
pytest tests/test_rule_logic.py -v --tb=short \
--cov=rules --cov-report=xml --cov-report=term
- name: Coverage gate
run: |
# Verifica che almeno l'80% delle regole abbia test
python -c "
import xml.etree.ElementTree as ET
tree = ET.parse('coverage.xml')
root = tree.getroot()
rate = float(root.attrib.get('line-rate', 0))
print(f'Coverage: {rate:.1%}')
assert rate >= 0.8, f'Coverage {rate:.1%} sotto la soglia 80%'
"
sigma-test:
name: sigma-test Inline Tests
runs-on: ubuntu-latest
needs: syntax-validation
steps:
- uses: actions/checkout@v4
- name: Install sigma-test
run: go install github.com/bradleyjkemp/sigma-test@latest
- name: Run sigma-test on all rules
run: sigma-test rules/ --exit-on-failure
coverage-check:
name: ATT&CK Coverage Check
runs-on: ubuntu-latest
needs: [syntax-validation, logic-testing]
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Check critical technique coverage
run: |
pytest tests/test_rule_logic.py::TestRuleCoverage -v
notify-on-failure:
name: Notify on Failure
runs-on: ubuntu-latest
needs: [syntax-validation, logic-testing, sigma-test, coverage-check]
if: failure()
steps:
- name: Notify Slack
run: |
curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
-H 'Content-type: application/json' \
--data '{"text": "Detection Rule CI fallita! PR: ${{ github.event.pull_request.html_url }}"}'
"""
탐지 규칙의 적용 범위 전략
탐지 규칙에 대한 좋은 적용 범위 전략은 코드 줄을 측정하는 것이 아니라 테스트된 동작. 권장되는 최소 목표:
- 각 규칙에는 최소 2개의 TP 테스트와 2개의 FP 테스트가 있어야 합니다.
- "높음" 및 "중요" 규칙에는 최소 3개의 TP와 3개의 FP가 있어야 합니다.
- "중요"로 분류된 ATT&CK 기술은 100% 적용되어야 합니다.
- FP 비율이 2% 미만인 모든 규칙에 대한 스트레스 테스트(100개의 무작위 이벤트)
시뮬레이터의 한계: 실제 SIEM 테스트를 대체하지 않습니다.
Python 시뮬레이터와 시그마 테스트는 훌륭한 사전 검증 도구이지만 그렇지 않습니다.
대상 SIEM 필드의 정규화를 완벽하게 시뮬레이션합니다. 규칙은
모든 로컬 테스트 통과 필드가 호출되므로 Splunk에서 실패할 수 있습니다.
process_path 대신에 Image. 항상 테스트를 추가하세요.
프로덕션에 배포하기 전에 실제 SIEM을 사용하여 스테이징 환경을 구축합니다.
결론 및 주요 시사점
탐지 규칙에 대한 단위 테스트는 오버헤드가 아니라 허용되는 투자입니다. 시간이 지나도 품질이 저하되지 않고 수백 개의 규칙 저장소를 유지합니다. 설명된 프레임워크를 사용하면 각 규칙에는 감지해야 하는 항목에 대한 명시적인 계약이 있습니다. 감지하지 말아야 할 사항은 변경될 때마다 자동으로 확인됩니다.
주요 시사점
- 탐지 규칙은 코드입니다. 다른 코드와 마찬가지로 테스트가 필요합니다.
- sigma-test를 사용하면 기본 YAML 형식의 규칙 식민지화 테스트가 가능합니다.
- pytest는 스트레스 테스트, 적용 범위, 매개변수화 등 고급 테스트를 위한 유연성을 제공합니다.
- 자동으로 생성된 종합 로그는 수동 이벤트보다 더 많은 사례를 포괄합니다.
- CI/CD 게이트는 잘못된 규칙이 프로덕션에 배포되는 것을 방지합니다.
- ATT&CK 적용 범위는 중요한 기술에 대한 탐지 격차를 식별합니다.
- 시뮬레이터는 준비 단계에서 실제 SIEM의 테스트를 대체하지 않습니다.
관련 기사
- 시그마 규칙: 범용 감지 논리 및 SIEM 변환
- Git 및 CI/CD를 사용한 코드로서의 탐지 파이프라인
- AI 지원 탐지: 시그마 규칙 생성을 위한 LLM
- MITRE ATT&CK 통합: 매핑 범위 격차







