Windows 이벤트 로그(.evtx)를 웹 화면에서 한눈에 보고 분석하는 Flask 기반 도구입니다.
이벤트 로그는 파일을 읽어서 분석하는 작업이므로 PowerShell(ps1) 대신 Python으로 구현했습니다.
cd piro-evtx
pip install -r requirements.txt
python main.py # http://127.0.0.1:5000
python main.py --port 8080 --host 0.0.0.0
python main.py --port 5050 # http://127.0.0.1:5050실행한 터미널에서는 Ctrl+C로 종료합니다. 여러 터미널에 띄워 둔 서버를 한 번에 모두 끄려면 아래 명령을 사용합니다.
# main.py로 실행 중인 python 프로세스를 모두 종료
Get-CimInstance Win32_Process -Filter "Name like 'python%'" |
Where-Object { $_.CommandLine -like '*main.py*' } |
ForEach-Object { Stop-Process -Id $_.ProcessId -Force }
# 특정 포트(예: 5050)를 쓰는 서버만 종료
Get-NetTCPConnection -LocalPort 5050 -State Listen -ErrorAction SilentlyContinue |
ForEach-Object { Stop-Process -Id $_.OwningProcess -Force }- 실행하면 첫 화면인 분석 대상에서 스캔된 대상을 고르고 대시보드·분석으로 들어갑니다.
- 폴더 내용을 바꿨다면 대상 카드의 재스캔 또는 사이드바의 전체 재스캔을 누릅니다. 새 파일과 크기·수정시각이 바뀐 파일만 다시 파싱하고, 삭제된 파일·폴더는 캐시에서도 지웁니다.
- 요구 사항: Python 3.10 이상,
flask,evtx(Rust 기반 고속 파서)
여러 PC의 로그를 함께 다루기 위해 .evtx가 직접 들어 있는 폴더 하나를 분석 대상 하나로 봅니다. 대상명은 그 폴더명입니다.
Target_eventlog/ ← 도구 폴더 기준 기본 스캔 경로
├─ HA26/ → 대상 "HA26"
│ └─ Windows PowerShell.evtx
└─ HA38/ → 대상 "HA38"
├─ ...LocalSessionManager%4Operational.evtx
└─ ...RDPClient%4Operational.evtx
- 스캔 경로: 도구 폴더 기준 기본 경로
Target_eventlog와, 화면에서 등록한 외부 폴더(data/targets.json).targets.json의 상대 경로도 도구 폴더를 기준으로 해석합니다. 스캔 경로 아래를 하위 폴더까지 찾습니다. Target_eventlog폴더가 없으면 서버 또는evtx-copy.ps1을 처음 실행할 때 자동으로 만듭니다.- 외부 폴더 등록: 로그를 복사하지 않고 원래 위치에서 바로 분석합니다. 등록 해제해도 원본 파일은 지우지 않습니다.
- 기본 경로 대상 등록 해제: 대상 카드의 등록 해제를 누르면 그 폴더는 전체 재스캔에서 제외됩니다(
targets.json의excluded). 다시 분석하려면 외부 폴더 등록에 그 경로를 입력해야 합니다. 등록 해제된 폴더 목록의 경로 입력을 누르면 입력칸이 채워집니다. 등록 해제된 대상명으로는 업로드할 수 없습니다. - 업로드: 대상명을 입력하고
.evtx를 올리면Target_eventlog/<대상명>/에 저장됩니다. - 대상 카드에는 로그 파일 수, 이벤트 수, 호스트, 기간, 채널, 탐지 결과 건수(등급별)와 주요 탐지 항목이 보입니다.
- 하나 또는 여러 대상을 체크하고 선택 대상 대시보드 / 분석으로 들어가면 모든 화면·API가 선택한 대상으로만 제한됩니다. 여러 개를 고르면 로그 파일과 이벤트 옆에 대상명이 함께 표시됩니다.
- 선택은 세션(쿠키)에 저장되고, 세션 키는
data/secret.key에 있어 서버를 다시 켜도 유지됩니다. - 같은 이름의 폴더가 여러 곳에 있으면
Logs (HA26)처럼 상위 폴더명을 붙여 구분합니다.
| 옵션 | 기본값 | 설명 |
|---|---|---|
--host |
127.0.0.1 |
바인딩 주소. 다른 PC에서 접속하려면 0.0.0.0 |
--port |
5000 |
포트 |
--debug |
끔 | Flask 디버그 모드(코드 변경 시 자동 재시작) |
piro-evtx/
├─ main.py # 실행 진입점. 앱 생성, 시작 시 폴더 동기화
├─ targets.py # 분석 대상: 폴더별 스캔 결과, 선택, 재스캔, 외부 폴더 등록, 업로드
├─ dashboard.py # 대시보드: 선택한 대상의 전체 현황
├─ evtxlist.py # 로그 목록: 채널별 설명, Event ID 분포
├─ analysis.py # 분석: 탐지 규칙, RDP 세션 재구성, 이벤트 조회 API
├─ store.py # evtx 파싱, SQLite 캐시, 필터·시간 분포 집계
├─ reference.py # Event ID 레퍼런스 조회·검색, /reference 화면과 API
├─ refdata/ # Event ID 레퍼런스 데이터. 모듈 하나 = 카테고리 하나 (자동 로드)
│ ├─ __init__.py # 모듈 형식 설명, load_categories()
│ ├─ logon.py, rdp.py, powershell.py, wmi.py, winrm.py ...
├─ requirements.txt
├─ templates/ # base, targets, dashboard, logs, analysis, reference (Jinja2)
├─ static/
│ ├─ css/app.css # 다크 테마
│ ├─ fonts/ # 로컬 폰트(woff2, 가변 굵기). 영문 Noto Sans, 한글 Noto Sans KR, 코드 Noto Sans Mono
│ └─ js/
│ ├─ charts.js # 외부 라이브러리 없는 SVG 차트 (타임라인, 도넛)
│ ├─ analysis.js # 분석 화면: 탭, 이벤트 탐색, 상세 패널
│ └─ reference.js # 레퍼런스 화면: 검색, 주제·등급·로그 파일 필터
├─ Target_eventlog/ # 기본 스캔 경로. 하위 폴더 하나 = 분석 대상 하나
│ ├─ PC-01/Security.evtx
│ └─ PC-02/...
├─ data/
│ ├─ cache.db # 파싱 결과 캐시 (자동 생성, 삭제하면 다시 파싱)
│ ├─ targets.json # {"roots": 외부 스캔 경로, "excluded": 기본 경로에서 등록 해제한 폴더}
│ └─ secret.key # 세션 키 (자동 생성)
관리자 PowerShell에서 evtx-copy.ps1을 실행하면 VSS 스냅샷으로 현재 PC의 EVTX 파일을 복사하고 SHA-256 해시를 검증합니다. 기본 저장 위치는 실행 위치가 아니라 스크립트가 있는 도구 폴더의 Target_eventlog입니다.
.\evtx-copy.ps1 -TargetName PC-01
# 외부 증거 드라이브를 쓸 때만 경로를 지정
.\evtx-copy.ps1 -TargetName PC-01 -DestinationRoot 'E:\EVTX'"중요한 부분만 볼지, 전체 값을 볼지"는 둘 다 하는 방식으로 구현했습니다.
- 전체 저장: 모든 레코드의 System 정보(시간, Event ID, 레벨, 채널, 컴퓨터, SID)와
EventData/UserData를
키=값형태로 펼쳐 SQLite에 저장합니다. 그래서 어떤 필드든 키워드로 검색할 수 있습니다. - 중요 항목 강조: 그 위에 다음 두 가지를 얹어 볼 것을 좁혀 줍니다.
refdata/의 등급(침해주의/주의/정보)과 설명. 같은 Event ID라도 채널별로 구분합니다(예: RDPClient 1102와 Security 1102).analysis.py의 탐지 규칙(RULES)이 찾아낸 항목을 출발지 IP·계정 등으로 묶은 결과.
- 시간은 UTC로 저장하고 화면에는 KST(UTC+9)로 표시합니다. 바꾸려면
store.py의TZ_HOURS,TZ_LABEL을 수정합니다. - 손상된 로그는 읽을 수 있는 부분까지만 적재하고, 오류 내용은 로그 목록과 대시보드에 표시합니다.
- 스캔 경로별로 대상 카드를 보여주고, 체크한 대상만 분석합니다(위 "분석 대상" 참고).
- 다른 화면 상단에 현재 선택한 대상이 표시되고, 변경을 누르면 이 화면으로 돌아옵니다.
- 대상을 고르지 않고 다른 화면에 들어가면 이 화면으로 돌아옵니다.
- 핵심 지표: 등록 로그 수, 전체 이벤트, 오류·경고, 탐지 결과, 원격 RDP 출처 IP, 로그 기간
- 이벤트 타임라인: 기간에 따라 시간/일/주/월 단위로 자동 집계하고 레벨별로 쌓아 표시. 막대를 클릭하면 그 구간의 이벤트로 이동
- 탐지 결과 요약, Event ID Top 12, 로그 파일별 레벨 비율, 레벨 분포, 레퍼런스 주의 이벤트
- 로그별로 무엇을 기록하는지와 분석 포인트(채널 설명은
evtxlist.py의CHANNELS) - Event ID 분포(이름, 등급, 건수, 최초/최종 시각). 행을 클릭하면 이벤트 탐색으로 이동
- 레퍼런스상 주요 ID 중 이 로그에 없는 항목 표시
- 선택한 대상별로 묶어서 표시
- 화면 오른쪽 상단 KST / UTC 전환: 기본은 KST(다른 화면과 동일). UTC를 고르면 탐지 결과·이벤트 탐색·RDP 세션·시간 필터·히스토그램·상세 패널이 UTC 기준으로 표시됩니다. URL에
?tz=utc가 붙습니다. - 탐지 결과: 규칙별 등급, MITRE ATT&CK, 묶음 표. 표의 행을 클릭하면 그 값(IP 등)으로 이벤트를 검색
- 이벤트 탐색
- 필터: 로그 파일, Event ID(
4624,4625포함 /-5858제외), 레벨, 키워드(EventData 전체), 시작·종료 시각 - 시작·종료는 날짜와 시간을 따로 입력하며 시간은 생략할 수 있습니다. 시작=종료=
2026-08-03처럼 날짜만 넣으면 그날 하루 전체를 봅니다. 종료에 시간을 넣으면 그 시각은 포함하지 않습니다(예:10:00→ 09:59:59까지). - 필터 결과의 시간 분포와 상위 Event ID, 100건 단위 페이지
- 행을 클릭하면 상세 패널이 열림: 레퍼런스 설명, EventData(레퍼런스 주요 필드는 주황색), System 정보, 같은 로그의 전후 이벤트, "같은 Event ID만 보기", "이 시각 ±10분 보기"
- 필터 상태가 URL에 남으므로 주소를 공유하면 같은 화면을 볼 수 있음
- 필터: 로그 파일, Event ID(
- RDP 세션: LocalSessionManager 이벤트로 세션 흐름을 재구성 (21 로그온 → 22 셸 시작 → 24 끊김 / 25 재연결 → 23 로그오프). 원격 IP는 주황색으로 표시
- 앱 안의 화면으로 열리며, 분석 대상을 고르지 않아도 볼 수 있습니다.
- 검색: Event ID, 이름, 설명, 필드명, MITRE 기법, 로그 파일명을 대상으로 하며 공백으로 나누면 모두 포함하는 항목만 찾습니다(AND).
Event ID와 정확히 같은 항목이 맨 앞에 옵니다.
/키로 검색칸 이동, ESC로 초기화 - 필터: 주제(로그온, RDP, PowerShell 등), 등급, 왼쪽 로그 파일 목록(파일 이름 필터 지원, 현재 조건의 건수 표시)
- 연관 Event ID를 누르면 그 ID로 검색하고 해당 항목을 펼칩니다.
- 필터 상태가 URL에 남습니다. 예:
/reference?q=4624,/reference?file=Security.evtx&topic=logon
일반 탐지 규칙은 analysis.py의 RULES에 정의되어 있습니다. Sysmon 채널은 refdata/sysmon.py에서 sev="warn"으로 등록된 Event ID를 탐지 결과에 자동으로 표시합니다. Sysmon 주의 이벤트는 조사 대상 요약이며, 기록 자체가 침해를 뜻하지 않습니다.
| 등급 | 규칙 | Event ID |
|---|---|---|
| 침해주의 | 보안 감사 로그 삭제 / 이벤트 로그 삭제 | Security 1102 / System 104 |
| 침해주의 | 감사 정책 변경 | 4719 |
| 침해주의 | 새 서비스 설치 | System 7045, Security 4697 |
| 침해주의 | 사용자 계정 생성 / 보안 그룹 멤버 추가 | 4720 / 4728·4732·4756 |
| 침해주의 | WMI 영구 이벤트 구독 등록 | 5861 |
| 침해주의 | 의심 PowerShell 스크립트 블록(-enc, IEX, DownloadString 등) | 4104 |
| 주의 | 로그온 실패 다발(같은 IP·계정 5회 이상) | 4625 |
| 주의 | RDP 로그온(Type 10) / 명시적 자격증명 사용 | 4624 / 4648 |
| 주의 | 원격 RDP 세션 로그온·재연결 / RDP 인증 성공 | LSM 21·25 / RCM 1149 |
| 주의 | 다른 서버로 RDP 접속(발신) | RDPClient 1024·1102 |
| 주의 | WinRM 원격 세션 생성(발신) / 원격 셸 요청 수신 | WinRM 6 / 91 |
| 주의 | 비표준 경로의 WMI 프로바이더 로드 | 5857 |
| 주의 | Sysmon 레퍼런스의 주의 이벤트 | Sysmon 1·2·3·6·8·255 (레퍼런스 등급에 따라 변경) |
| 정보 | RDP 리스너 연결 수신, WMI 작업 오류, WinRM 작업 요약 | 261, 5858, 145 |
일반 규칙을 추가하려면 RULES에 항목을 하나 더 넣습니다. Sysmon 주의 등급은 refdata/sysmon.py의 sev 값을 수정합니다.
{"key": "my_rule", "sev": "warn", "title": "규칙 이름", "eids": [4624], "channels": SECURITY,
"where": "json_extract(e.data, '$.LogonType') = 3", # 선택: 추가 SQL 조건
"group": ["IpAddress", "TargetUserName"], # 묶을 EventData 필드 (비우면 최근 20건 샘플)
"labels": ["출발지 IP", "계정"], "min_count": 1,
"mitre": "T1021", "desc": "설명"},- Event ID 설명은
refdata/폴더의 Python 모듈에 있습니다. 수정 후 서버를 재시작하면 반영됩니다. - 새 로그 파일(채널)은 기존 카테고리 모듈의
ITEMS에 추가하거나, 새 모듈 파일을 만들면 자동으로 읽습니다.
# refdata/taskscheduler.py
CATEGORY = "작업 스케줄러"
FILE = "Microsoft-Windows-TaskScheduler/Operational.evtx" # 화면 표시용
SOURCE = "Microsoft-Windows-TaskScheduler%4Operational.evtx" # 실제 파일명. 채널 매칭에 사용
TAGS = ["system"] # reference.TOPICS의 키
ORDER = 130 # 표시 순서
ITEMS = [
{
"id": "106", "name": "예약 작업 등록", "en": "Task registered", "sev": "warn", "tags": ["system"],
"desc": "새 예약 작업 등록. <b>TaskName</b>과 등록 사용자로 지속성 여부 확인.", # HTML(<b>, <br>) 사용 가능
"fields": ["TaskName", "UserContext"],
"rel": ["140", "200"],
"attack": "T1053.005 예약 작업",
},
]- 한 카테고리 안에서 파일이 다른 항목은
"source": "...evtx"를 따로 지정합니다(예:rdp.py의 1149, 1024). - 주제 필터를 늘리려면
reference.py의TOPICS에(키, 라벨)을 추가합니다. - 레퍼런스에 없는 ID는 화면에 "미등록"으로 표시됩니다(예: LocalSessionManager 32, 34, 54).
| 경로 | 설명 |
|---|---|
GET /api/events |
선택한 대상의 이벤트 목록(대상 미선택 시 409). 파라미터: file, channel, eid, level(error/warn/info), q, start, end(KST, YYYY-MM-DDTHH:MM), page, size(10~500), order(asc/desc). 시간 분포(histogram)와 상위 ID(top)를 함께 반환 |
GET /api/events/<id> |
이벤트 상세: EventData, System, 레퍼런스, 전후 이벤트 |
GET /api/reference |
레퍼런스 검색. 파라미터: q, topic, sev(crit/warn/info), file(로그 파일명). 로그 파일별 건수(files)를 함께 반환 |
POST /targets/select |
분석 대상 선택(ids 여러 개) |
POST /targets/scan |
전체 스캔 경로 재스캔 |
POST /targets/<id>/scan |
대상 하나만 재스캔 |
POST /targets/add |
외부 스캔 경로 등록(path) |
POST /targets/<id>/exclude |
기본 경로 대상 등록 해제(전체 재스캔에서 제외) |
POST /targets/roots/remove |
외부 스캔 경로 등록 해제(root) |
POST /targets/upload |
Target_eventlog/<name>/에 .evtx 업로드(name, files) 후 파싱 |
외부 CDN 없이 static/fonts의 파일만 사용하므로 인터넷이 없는 분석 PC에서도 같은 화면으로 보입니다.
| 파일 | 용도 | 출처 |
|---|---|---|
NotoSans-latin.woff2, NotoSans-latin-ext.woff2 |
영문 | Fontsource (Noto Sans, variable) |
NotoSansKR-korean.woff2 |
한글 | Google Fonts NotoSansKR[wght].ttf에서 한글·영문·기호만 추려 변환 |
NotoSansMono-latin.woff2 |
Event ID, 시간, IP 등 고정폭 | Fontsource (Noto Sans Mono, variable) |
- Noto Sans에는 한글 글자가 없으므로 같은 문장 안에서도 한글은 자동으로 Noto Sans KR로 표시됩니다.
- 한글 폰트는 한자를 제외해 용량을 줄였습니다(10MB → 1.3MB). 다시 만들 때는 아래 명령을 사용합니다.
pip install fonttools brotli
pyftsubset "NotoSansKR[wght].ttf" --flavor=woff2 --layout-features='*' --output-file=static/fonts/NotoSansKR-korean.woff2 `
--unicodes="U+0020-007E,U+00A0-00FF,U+1100-11FF,U+2000-206F,U+2190-21FF,U+2460-24FF,U+25A0-25FF,U+2600-27BF,U+3000-303F,U+3131-318F,U+3200-321E,U+AC00-D7A3,U+FF01-FF5E"- 라이선스: SIL Open Font License 1.1 (
static/fonts/OFL.txt)
- Flask 개발 서버로 동작합니다. 여러 명이 함께 쓰려면
waitress등 WSGI 서버로 실행하는 것이 좋습니다. - 인증 기능이 없으므로
--host 0.0.0.0으로 열 때는 접근 가능한 네트워크를 제한하세요.