SPL 치트시트¶
한 줄 요약
SPL은 파이프라인입니다: 첫 부분이 이벤트를 찾고(인덱스, 소스타입, 키워드), 그 뒤의 모든 |가 앞에서 온 것을 변환합니다 — 거르기, 계산, 묶기, 세기, 정렬, 표시. 명령 25개 정도만 익히면 침해사고 대응 작업의 95%를 할 수 있습니다.
검색의 구조¶
index=botsv3 sourcetype=WinEventLog EventCode=4625 earliest=-24h latest=now
| eval user=lower(Account_Name)
| stats count by user, src_ip
| where count > 10
| sort - count
| head 20
위에서 아래로 읽으세요: 1) botsv3 인덱스에서 최근 24시간의 로그온 실패 이벤트(4625)를 가져오고; 2) 소문자 user 필드를 만들고; 3) 사용자 + 출발지 IP별로 이벤트를 세고; 4) 10개를 넘는 쌍만 남기고; 5) 큰 것부터; 6) 상위 20개. 각 줄은 앞 줄이 만든 행을 받습니다.
경험칙: 항상 인덱스를 지정하세요 (index=*는 느리고 운영 환경에서는 금지되는 경우가 많음); 가장 많이 걸러 내는 조건을 앞에 두세요 (자유 텍스트보다 EventCode=4625 먼저); where로 늦게 거르지 말고 검색창에서 일찍 거르세요; 결과가 많으면 table 대신 stats를 쓰세요.
검색 시점 필터링 (첫 | 앞)¶
| 문법 | 의미 |
|---|---|
index=botsv3 |
어느 인덱스 |
sourcetype=WinEventLog · source="WinEventLog:Security" · host=FYODOR-L |
메타데이터 필드 |
EventCode=4688 |
필드 = 값 (문자열 값은 대소문자 무시) |
EventCode IN (4624, 4625, 4648) |
목록 중 하나 |
user=admin* |
와일드카드 (끝에 쓰면 빠름; 앞에 쓴 *admin은 느림) |
NOT user=SYSTEM · user!=SYSTEM |
제외 (NOT field=x는 필드가 없는 이벤트도 남기고; field!=x는 그런 이벤트를 버림) |
"cmd.exe /c" |
구문 (따옴표) |
mimikatz OR procdump · A AND B (AND is implicit) |
논리 연산 |
earliest=-7d@d latest=@d |
시간 수식어 — @d는 자정에 맞춤; -7d@d = 7일 전 자정 |
earliest="09/15/2026:00:00:00" |
절대 시각 |
src_ip=10.0.0.0/24 |
IP 필드에는 CIDR 사용 가능 |
TERM(10.0.0.5) |
원시 토큰을 정확히 일치, 빠르고 분할(segmentation) 문제를 피함 |
CASE(Mimikatz) |
대소문자 구분 키워드 |
명령 — 매일 쓰는 것들¶
각 행: 하는 일, 그리고 바로 복사해 쓸 수 있는 예시.
행 거르기와 다듬기¶
| 명령 | 하는 일 | 예시 |
|---|---|---|
search |
파이프라인 중간에 다시 거르기 (검색창과 같은 문법) | … \| search count > 5 user!=svc* |
where |
식으로 거르기 (함수, 필드 간 비교) | \| where bytes_out > 10*bytes_in AND like(dest, "10.%") |
dedup |
고유 값마다 첫 이벤트만 남기기 | \| dedup user, host · \| dedup 3 host sortby -_time |
head / tail |
처음 / 마지막 N행 | \| head 50 |
sort |
행 정렬 (- = 내림차순; 0 = 1만 행 제한 없음) |
\| sort 0 - count, +user |
reverse |
순서 뒤집기 | |
fields |
열 남기기 (또는 -로 제거) — 속도를 높임 |
\| fields _time, host, user, CommandLine · \| fields - _raw |
table |
열을 표로 보여 주기 (표시 전용) | \| table _time host user CommandLine |
rename |
필드 이름 바꾸기 (와일드카드 가능) | \| rename Account_Name as user, "Source Network Address" as src_ip |
regex |
필드에 정규식을 걸어 행 거르기 | \| regex CommandLine="(?i)-enc\|-e\s+[A-Za-z0-9+/]{20,}" |
새 필드 계산¶
| 명령 | 하는 일 | 예시 |
|---|---|---|
eval |
함수로 필드 만들기/수정 | \| eval MB=round(bytes/1024/1024,2), user=lower(user) |
eval + if / case |
조건에 따른 값 | \| eval type=case(Logon_Type==10,"RDP", Logon_Type==3,"Network", true(),"Other") |
eval + coalesce |
여러 필드 중 처음으로 null이 아닌 값 | \| eval user=coalesce(Account_Name, user, User) |
eval + 문자열 함수 |
len, upper, lower, substr, replace, split, mvindex, trim, urldecode |
\| eval domain=mvindex(split(query,"."),-2) |
eval + 시간 함수 |
strftime, strptime, relative_time, now() |
\| eval day=strftime(_time,"%Y-%m-%d") |
eval + match / like / cidrmatch |
패턴 검사 (true/false 반환) | \| eval internal=if(cidrmatch("10.0.0.0/8",dest_ip),"yes","no") |
rex |
정규식 이름 그룹으로 필드 추출 | \| rex field=CommandLine "-enc\s+(?<b64>[A-Za-z0-9+/=]+)" — rex와 필드 참고 |
rex mode=sed |
필드 안에서 찾아 바꾸기 | \| rex field=user mode=sed "s/^CORP\\\\//" |
spath |
JSON/XML에서 필드 꺼내기 | \| spath input=_raw path=Event.EventData.Data{@Name} |
lookup |
CSV/KV 룩업으로 보강 | \| lookup asset_inventory ip as dest_ip OUTPUT owner, criticality |
iplocation |
GeoIP 위치 | \| iplocation src_ip |
fillnull |
null 값 채우기 | \| fillnull value="-" user, src_ip |
makemv / mvexpand |
문자열을 다중값으로 쪼개기 / 값마다 한 행으로 | \| makemv delim="," answers \| mvexpand answers |
convert |
포맷 변환 (epoch → 읽기 쉬운 형식 등) | \| convert ctime(first) ctime(last) |
묶기, 세기, 요약 (SPL의 핵심)¶
| 명령 | 하는 일 | 예시 |
|---|---|---|
stats |
모든 행을 집계, by로 묶음 지정 가능 |
\| stats count, dc(dest) as hosts, values(CommandLine) as cmds by user |
stats 함수 |
count, dc (고유 개수), sum, avg, min, max, stdev, median, perc95, values (고유 목록), list (전부, 순서대로), first, last, earliest, latest, range |
\| stats earliest(_time) as first, latest(_time) as last, count by host |
eventstats |
stats와 같지만 모든 행을 유지하고 결과를 덧붙임 |
\| eventstats avg(count) as avg_count by user |
streamstats |
행 순서대로 누적/이동 계산 | \| streamstats current=f last(_time) as prev by src, dest \| eval gap=_time-prev |
timechart |
시간 구간으로 집계 (차트용) | \| timechart span=1h count by EventCode |
bin (a.k.a. bucket) |
_time(또는 숫자)을 구간으로 반올림한 뒤 stats |
\| bin _time span=10m \| stats count by _time, src_ip |
chart |
2차원 피벗 표 | \| chart count over user by Logon_Type |
top / rare |
가장 흔한 / 가장 드문 값과 개수, 비율 | \| top limit=20 Image · \| rare limit=20 parent_process |
tstats |
색인된 필드 / 데이터 모델 / 가속 데이터에 대한 빠른 통계 | \| tstats count where index=botsv3 sourcetype=WinEventLog by _time span=1h, host |
transaction |
필드 + 시간 간격으로 이벤트를 세션으로 묶기 (느림; stats 선호) |
\| transaction Logon_ID maxspan=8h |
addtotals / addcoltotals |
행/열 합계 |
검색 합치기¶
| 명령 | 하는 일 | 예시 |
|---|---|---|
append |
서브서치 결과를 아래에 붙이기 | \| append [search index=zeek sourcetype=zeek:conn …] |
join |
필드로 SQL식 조인 (제한: 5만 행, 느림) | \| join type=left uid [search index=zeek sourcetype=zeek:http \| fields uid, host, uri] |
검색창의 [ subsearch ] |
한 검색의 결과를 다른 검색의 필터로 사용 (1만 행 제한) | index=botsv3 sourcetype=WinEventLog [search index=botsv3 EventCode=1102 \| fields host] |
OR와 함께 쓰는 stats … by |
join의 빠른 대안: 두 소스타입을 한 번에 검색하고 공통 키로 stats values() |
(sourcetype=zeek:conn OR sourcetype=zeek:http) \| stats values(host) as http_host, sum(orig_bytes) as up by uid |
inputlookup / outputlookup |
룩업 테이블 읽기 / 쓰기 | \| inputlookup known_admins.csv · \| outputlookup baseline_ua.csv |
map |
결과 행마다 검색 실행 (느림, 특수 용도) |
표시와 관리¶
| 명령 | 하는 일 |
|---|---|
fieldsummary |
필드별 통계 (개수, 고유 값, 상위 값) — 모르는 데이터를 처음 볼 때 |
metadata type=sourcetypes index=botsv3 |
소스타입 목록과 처음/마지막 시각, 개수 (즉시) |
\| tstats count where index=botsv3 by sourcetype |
같은 일, 더 빠르고 유연함 |
eventcount summarize=false index=* |
인덱스별 이벤트 수 (*를 써도 되는 경우 — 메타데이터만) |
rest /services/data/indexes |
인덱스 목록 |
typeof, isnull, isnotnull |
eval/where 안에서 타입/null 검사 |
format |
서브서치 행을 (a OR b OR c) 식으로 바꾸기 |
collect |
결과를 요약 인덱스에 쓰기 |
재사용할 패턴¶
기준선 vs. 오늘 (새로운 것 찾기)
index=botsv3 sourcetype=WinEventLog EventCode=4688 earliest=-30d@d latest=@d
| stats count by New_Process_Name
| eval period="baseline"
| append [ search index=botsv3 sourcetype=WinEventLog EventCode=4688 earliest=@d
| stats count by New_Process_Name | eval period="today" ]
| stats values(period) as seen by New_Process_Name
| where mvcount(seen)=1 AND seen="today"
첫 검색은 오늘 이전 30일 동안 본 모든 프로세스 이름을 baseline 레이블로 나열합니다; append는 오늘 목록을 today 레이블로 더합니다; stats values(period)는 이들을 프로세스마다 레이블 집합을 가진 한 행으로 합칩니다; 레이블이 today뿐인 프로세스는 기준선에 한 번도 나타나지 않은 것입니다.
희소성 (긴 꼬리)
index=botsv3 sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventID=1
| stats dc(host) as hosts, count by Image
| where hosts <= 2
| sort count
dc(host)는 각 바이너리를 실행한 서로 다른 머신 수를 셉니다; 전체 중 한두 대에만 있는 프로그램은 볼 가치가 있고, sort count(오름차순) 후 가장 드문 것이 맨 위에 옵니다.
시간 구간별 급증
index=botsv3 sourcetype=WinEventLog EventCode=4625
| bin _time span=5m
| stats count, dc(Account_Name) as users by _time, Source_Network_Address
| where count > 20
bin은 각 이벤트 시각을 속하는 5분 구간으로 내림합니다; 그다음 stats가 구간별·출발지별로 실패 수와 고유 계정 수를 셉니다; 한 IP에서 5분에 20회 넘는 실패는 무차별 대입입니다 (사용자가 많으면 = 스프레이).
처음/마지막으로 본 시각
index=botsv3 sourcetype=WinEventLog EventCode=4624 Logon_Type=10
| stats earliest(_time) as first, latest(_time) as last, count by Account_Name, Source_Network_Address, host
| convert ctime(first) ctime(last)
| sort first
earliest/latest는 묶음마다 처음과 마지막 타임스탬프를 돌려줍니다; convert ctime()이 읽기 쉽게 바꿉니다; first로 정렬하면 RDP 출발지/계정 쌍이 처음 나타난 시점이 보입니다 — 사고 시간대에 새로 생긴 쌍이 흥미로운 행입니다.
속도와 예절¶
- 인덱스와 소스타입을 지정하고, 시간 범위를 좁히고, 키워드는
where가 아니라 검색창에 넣으세요. fields는 일찍,table은 늦게.stats가transaction보다 낫고;stats … by key가join보다 낫습니다.- 색인된 필드(
host,source,sourcetype,_time, 가속 데이터 모델의 모든 필드)로 셀 때는tstats— 몇 자릿수 더 빠릅니다. - 앞쪽 와일드카드(
*admin)와index=*는 피하세요. - 유용한 검색은 Reports로 저장하고, 자리표시자를 넣은 패턴을 이 핸드북에 적어 두세요.