rex, 필드 추출, eval¶
한 줄 요약
필요한 필드가 없으면 만드세요: rex는 정규식 이름 그룹으로 텍스트에서 값을 꺼내고, spath는 JSON/XML을 따라가며, eval은 가진 것을 다시 다듬습니다 — 그리고 검색에서 검증된 패턴은 영구 추출로 승격시켜 아무도 다시 입력하지 않게 하세요.
rex 핵심¶
- 기본값은
field=_raw. 캡처마다(?<fieldname>…)으로 이름을 붙이면 그게 새 필드가 됩니다. - 정규식 방언은 PCRE입니다. Splunk는
"…"안의 백슬래시를 두 번 해석합니다: 패턴이 큰따옴표 안에 있으면\d를\\d로 쓰세요 — 더 간단하게는, 패턴을 짧게 유지하고 대개 그대로 살아남는\d,\s,\w를 쓰고 UI에서 시험해 보세요. max_match=0을 추가하면 일치하는 모든 것을 다중값 필드로 캡처합니다 (기본값 1).- 맨 앞의
(?i)= 대소문자 무시. mode=sed는rex를 찾아 바꾸기로 만듭니다:| rex field=x mode=sed "s/old/new/g".
실제 예시¶
Base64로 인코딩된 PowerShell
index=botsv3 sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventID=1 Image="*powershell.exe"
| rex field=CommandLine "(?i)-(?:e|en|enc|encodedcommand)\s+(?<b64>[A-Za-z0-9+/=]{20,})"
| where isnotnull(b64)
| eval decoded=replace(base64decode(b64),"\x00","")
| table _time, host, User, decoded
정규식은 -e, -en, -enc, -encodedcommand 중 아무거나(대소문자 무시), 공백, 그리고 20자 이상의 base64 문자를 b64로 캡처합니다. where isnotnull(b64)는 캡처에 성공한 행만 남깁니다. base64decode(Splunk 8+; 구버전은 사용자 정의 명령 필요)가 텍스트로 되돌리고, replace는 PowerShell이 글자 사이에 넣는 UTF-16 널 바이트를 지웁니다.
도메인 없는 사용자명
CORP\jsmith를 domain=CORP, username=jsmith로 캡처합니다. 백슬래시 네 개는 Splunk의 따옴표 해석 후 정규식 \\(백슬래시 문자 하나)가 됩니다.
자유 텍스트에서 IP + 포트
| rex field=Message "Source Network Address:\s+(?<src_ip>\d{1,3}(?:\.\d{1,3}){3})\s+Source Port:\s+(?<src_port>\d+)"
이벤트 안의 모든 IP (다중값)
| rex max_match=0 "(?<ips>\b(?:\d{1,3}\.){3}\d{1,3}\b)"
| mvexpand ips
| search ips!=127.0.0.1 ips!=0.0.0.0
max_match=0이 모든 IP를 ips에 모으고, mvexpand가 IP마다 한 행씩 만들어서 거르고 셀 수 있게 합니다.
경로에서 파일명과 확장자
URL / SNI에서 도메인
| rex field=url "^(?:https?://)?(?<fqdn>[^/:]+)"
| eval domain=mvindex(split(fqdn,"."),-2).".".mvindex(split(fqdn,"."),-1)
split은 FQDN을 점 기준으로 다중값 목록으로 쪼개고, mvindex(...,-2)와 -1은 마지막 레이블 두 개를 고르며, .이 이어 붙입니다 (mail.evil-example.com → evil-example.com). 대부분의 TLD에 충분하지만 co.uk 같은 경우는 룩업이 필요합니다.
애드온 없는 Sysmon XML
| rex field=_raw "<Data Name='Image'>(?<Image>[^<]+)"
| rex field=_raw "<Data Name='CommandLine'>(?<CommandLine>[^<]+)"
| rex field=_raw "<Data Name='ParentImage'>(?<ParentImage>[^<]+)"
| rex field=_raw "<EventID>(?<EventID>\d+)</EventID>"
rex 하나가 <Data Name='…'>value</Data> 요소 하나를 잡습니다. 더 나은 방법: 아래 spath, 또는 애드온 설치.
JSON / XML에는 spath¶
| spath # auto-extract everything (JSON) — fine for small results
| spath path=userIdentity.arn output=who # one path, renamed
| spath path=Event.EventData.Data{@Name} # XML attributes → multivalue
| spath input=Message path=alert.signature # spath on a field that contains JSON
Sysmon XML이라면 보통 spath에 mvzip/mvexpand를 함께 쓰거나 그냥 애드온을 설치합니다 — XML 모양(<Data Name="X">value) 때문에 여기서는 spath보다 rex가 더 간단합니다.
이미 있는 자동 추출¶
rex를 쓰기 전에 Splunk가 이미 주는 것을 확인하세요: Interesting Fields 패널; | fieldsummary; 원시 텍스트의 key=value 쌍은 자동으로 추출됨(user=admin → user); Field Extractor(Extract New Fields → 텍스트 강조 → Splunk가 정규식을 작성)는 rex를 만들고 저장하는 GUI 방식입니다.
eval — 실제로 쓰는 함수¶
| 필요 | 함수 |
|---|---|
| 조건 | if(cond, a, b) · case(c1, v1, c2, v2, true(), default) · coalesce(a, b, c) · nullif(a, b) |
| 문자열 | lower, upper, len, substr(s, start, len), replace(s, regex, repl), trim, ltrim, rtrim, split(s, delim), . (이어 붙이기), urldecode, md5, sha1, sha256 |
| 검사 | like(s, "pat%"), match(s, "regex"), cidrmatch("10.0.0.0/8", ip), isnull, isnotnull, in(field, "a","b"), searchmatch("user=admin*") |
| 숫자 | round(x, 2), floor, ceil, abs, pow, log, tonumber(s), tostring(n, "commas"), random() |
| 시간 | now(), strftime(t, fmt), strptime(s, fmt), relative_time(t, "-1d@d"), time() |
| 다중값 | mvcount, mvindex(mv, i), mvjoin(mv, ","), mvfilter(match(mv, "x")), mvdedup, mvappend, mvzip, split, mvsort |
| 인코딩 | base64decode / base64encode (8.1+), printf, tostring(x,"hex") |
| 타입 | typeof(x), tonumber, tostring |
작은 eval 레시피¶
| eval user=lower(coalesce(user, Account_Name, User)) # normalise usernames across sourcetypes
| eval is_admin=if(match(user,"(?i)admin|adm_|svc_"),1,0)
| eval logon_type_name=case(Logon_Type==2,"Interactive",Logon_Type==3,"Network",Logon_Type==10,"RemoteInteractive",Logon_Type==9,"NewCredentials",true(),"Other")
| eval internal=if(cidrmatch("10.0.0.0/8",dest_ip) OR cidrmatch("192.168.0.0/16",dest_ip) OR cidrmatch("172.16.0.0/12",dest_ip),"int","ext")
| eval MB_out=round(bytes_out/1048576,2)
| eval cmd_len=len(CommandLine), has_b64=if(match(CommandLine,"[A-Za-z0-9+/]{50,}={0,2}"),1,0)
| eval entropy_hint=len(replace(lower(query),"[aeiou0-9.-]",""))/len(query) # crude consonant ratio for DGA-ish names
| eval day=strftime(_time,"%Y-%m-%d"), hour=strftime(_time,"%H")
| eval hash_sha256=mvindex(split(mvindex(split(Hashes,"SHA256="),1),","),0) # Sysmon Hashes field "MD5=…,SHA256=…,IMPHASH=…"
마지막 줄: split(Hashes,"SHA256=")이 문자열을 SHA256=에서 자르고, mvindex(...,1)이 그 뒤를 가져오며, 그것을 ,로 다시 쪼개 0번을 고르면 해시만 남습니다.
룩업 — 보강과 허용 목록¶
# CSV with columns ip,owner,criticality uploaded as lookup "assets"
| lookup assets ip as dest_ip OUTPUTNEW owner, criticality
# Allow-list: drop rows whose process is in known_good.csv (column: process)
| lookup known_good process as Image OUTPUT process as known
| where isnull(known)
# Build a baseline once, reuse it
index=botsv3 sourcetype=stream:http earliest=-30d@d latest=@d | stats count by http_user_agent | outputlookup baseline_ua.csv
OUTPUTNEW는 비어 있는 필드만 채우고, OUTPUT은 덮어씁니다. 허용 목록 요령: 목록과 결합한 뒤 룩업이 실패한(isnull) 행 — 즉 목록에 없는 것 — 만 남깁니다.
rex를 영구 추출로 승격하기¶
rex가 동작하면 검색 시점에 자동으로 적용되도록 저장하세요: Settings → Fields → Field extractions → New에서 소스타입 + 같은 정규식(field=는 _raw가 아닐 때만 남기고), 또는 props.conf에:
[XmlWinEventLog:Microsoft-Windows-Sysmon/Operational]
EXTRACT-sysmon_image = <Data Name='Image'>(?<Image>[^<]+)
EXTRACT-sysmon_cmd = <Data Name='CommandLine'>(?<CommandLine>[^<]+)
함께 유용한 것: 필드 별칭(Account_Name → user), 계산 필드(한 번 저장한 eval), 이벤트 유형(저장된 검색 조건, 예: eventtype=win_failed_logon), 태그(tag=authentication). 애드온이 CIM 필드를 나타나게 하는 방법이 이것들입니다.
추출 디버깅¶
- 아무것도 추출되지 않나요? 이벤트 하나로 정규식을 시험하세요:
| head 1 | rex … | table b64— 그다음 조건을 느슨하게. - 백슬래시 문제? 패턴을 Field Extractor GUI에 넣거나, 백슬래시 문자 클래스는
[\\\\]를 쓰세요. - 필드 이름이 틀렸나요? 철자와 대소문자를 확인하세요 — 필드 이름은 대소문자를 구분하고(
EventCode≠eventcode), 값은 대개 구분하지 않습니다. - 사이드바에는 필드가 보이는데
stats는 null이라고 하나요? 일부 이벤트에만 있을 수 있습니다 —| stats count(field) count로 비율을 보세요. - 다중값 함정:
stats by Account_Name은 값마다 나뉩니다. 먼저mvindex나mvjoin을 쓰세요.