에스투더블유 FDE(AI 엔지니어링)는 고객 문제를 정의부터 해결까지 끌고 가며, 비정형·반정형 데이터를 OCR/VLM/LLM 으로 처리해 구조화하고 기업의 데이터를 자산화하는 일을 핵심으로 합니다. 위협 인텔 리포트라는 비정형 문서에서 IOC 를 정규식 형식 검증과 원문 근거로 걸러 구조화 레코드로 적재하는 이 게이트는 그 핵심(문서 구조화·자산화·규칙 기반 품질 점검·LLM 추출 근거화)을 정면으로 입증합니다. 실제 위협 판정·다크웹 분석·악성 여부 같은 보안·위협 인텔 도메인 전문성은 포트폴리오로 증명되지 않으므로 합성 리포트로 기술만 보이고, 그 깊이는 면접에서 솔직히 다룹니다. 추출 IOC 를 형식 규칙·원문 근거로 잇는 사슬은 단순 LLM 추출 맥락으로는 그대로 만들 수 없습니다.
OCR·LLM 으로 비정형·반정형 문서를 구조화 데이터로 가공해 자산화하는 일이 주요업무입니다. 리포트에서 IOC 를 뽑아 구조화 레코드로 적재하는 이 게이트가 정면으로 정렬됩니다.
Rule Engine/RBS 로 문제를 풀고 스키마·정합성·누락 등 데이터 품질을 점검합니다. 형식 규칙과 근거 대조로 적재 여부를 결정하는 게이트와 정렬됩니다.
tool-calling·RAG·agent loop 기반 LLM 에이전트를 설계·개발하고 OCR/VLM/LLM 으로 데이터를 처리합니다. LLM 추출에 원문 근거 인용을 강제하는 이 게이트와 정렬됩니다.
위협 인텔 리포트(비정형 문서)에서 LLM 이 뽑은 IOC 후보(원본 표기·정규화 값·타입·근거 span)를 받아, 규칙(정규식 형식 위반·원문 미근거·정규화 실패·미등록 타입)으로 적재할지 검토할지 막을지 정합니다. 적재 여부는 정규식 형식 검증과 원문 substring 근거로 결정하고, LLM 은 추출·근거 인용 보조만 맡습니다. IOC 가 진짜 악성인지 판정하는 도구가 아니라, 리포트에 적힌 지표를 형식·근거 정합성으로 걸러 구조화 자산에 적재할지 정하는 게이트입니다.
에스투더블유 FDE(AI 엔지니어링). 위협 인텔 리포트에서 IOC 를 눈으로 하나씩 뽑아 형식이 맞는지, 원문에 실제로 있는지 대조하며 자산으로 정리하던 일을, 한 게이트에서 추출·형식·근거 결과를 함께 보고 싶은 사람입니다.
추출 IOC 를 한 스키마로 묶고 정규식 형식 규칙과 원문 근거 대조를 붙이면, (a) 형식이 깨진 값과 원문에 없는 지어낸 지표를 규칙으로 거르고, (b) 적재 근거를 원문 인용으로 풀며, (c) 여러 리포트를 끊김 없이 구조화 자산으로 적재합니다.
위협 리포트에서 IOC 를 추출하고 정규식 형식·원문 근거·정규화를 검사해 통과만 구조화 레코드로 적재하는 동작하는 게이트입니다. 리포트는 합성 위협 리포트 더미로 시작하고, 저장은 PostgreSQL 한 곳으로 가볍게 둡니다.
적재 판정을 심은 정답 IOC·검토자 2명 라벨과 비교해 정밀도·재현율과 일치도를 보여주는 화면입니다.
Kafka 로 들어오는 신규 리포트를 Airflow 로 수집·추출·검증·적재까지 자동화하고, Spark 로 도메인·IOC 타입별 적재 정합성과 거름 사유 분포를 집계해 약한 추출을 보여주는 화면입니다.
추출 시나리오 15종(정상 IOC·지어낸 지표·형식 위반·defang 표기)을 넣으면, 추출·형식 검증·근거·적재 결정이 몇 초 안에 나오는 걸 시연합니다
일부러 만든 악성·결측 입력 30종(defang 표기 흔들림·형식 위반·원문 미근거·미등록 타입·근거 span 결측)을 넣어도 출력 형식(스키마)이 한 번도 안 깨집니다
더미에 심은 정답 IOC 45건 중 적재/거름 판정이 일치한 게 40건(약 89%)입니다. 단, 정답·데이터를 모두 작성자가 만든 합성 기준이라 '성능 검증'이 아니라 '데모 조건에서의 동작'임을 명시합니다
진짜 성능 검증은 여기서 멈춥니다. 위 수치는 전부 작성자가 만든 더미데이터 자가채점이며, 실제 정밀도·재현율은 에스투더블유의 실 위협 리포트와 실 검토자가 있어야 측정 가능합니다. 이 게이트는 그 검증을 바로 돌릴 수 있는 상태까지 만듭니다
이건 고객·실데이터 없이 혼자 만든 포트폴리오입니다. 모든 수치는 합성 데이터 자가채점이라 성능 증명이 아닙니다. 면접에서는 데모 동작과 실검증 준비 상태로 솔직히 설명하는 게 오히려 신뢰를 줍니다
정답을 작성자가 만들었으니 잘 맞는 게 당연합니다. 그래서 defang 표기 흔들림·형식 위반·원문 미근거·미등록 타입 같은 경계 케이스를 더미에 일부러 섞어, 쉬운 추출만 통과시키는 게 아님을 보여줍니다
이 포트폴리오가 입증하는 것은 비정형 문서에서의 LLM 추출 + 정규식 형식 검증 + 원문 근거 대조 + 구조화 적재 기술입니다. 실제 위협 판정·다크웹 분석·악성 여부 결정 같은 에스투더블유의 보안·위협 인텔 도메인 전문성은 포트폴리오로 증명되지 않으므로 주장하지 않으며, 그 깊이는 실제 보안 분석가의 몫이고 면접에서 모르면 모른다고 말하는 편이 낫습니다. 데모는 합성 위협 리포트로 기술만 입증합니다
LLM 이 뽑은 IOC 는 그럴듯해도 원문에 없을 수 있습니다. IOC 마다 원문 span 인용을 달게 하고, defang 정규화 후에도 원문에서 못 찾으면 '미근거'로 걸러 구조화 자산에 적재하지 않습니다
IOC 표기가 제각각입니다(hxxp·[.]·대소문자). 정규화로 같은 값으로 묶고, 못 묶은 건 추측하지 말고 '미정규화'로 알립니다. 실제 도입 시 리포트 포맷·IOC 타입 사전이 더미와 다르므로 정규식·enum 을 다시 맞춰야 합니다(도입 단계 과제로 분리)
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
위협 리포트에서 뽑은 IOC 추출·적재 로그. 형식·근거 규칙이 가장 먼저 질의하는 테이블입니다. defang 표기 흔들림·형식 위반·원문 미근거·미등록 타입을 일부러 섞고, label 로 정답을 심어 자가채점합니다.
| ioc_id | report_id | raw_value | normalized_value | ioc_type | format_valid | grounded | source_span | label | ts |
|---|---|---|---|---|---|---|---|---|---|
| ioc_001 | rpt_01 | malicious-c2[.]com | malicious-c2.com | domain | true | true | C2 도메인 malicious-c2[.]com 이 확인되었습니다 | load | 2026-04-02T09:00 |
| ioc_002 | rpt_01 | 203.0.113.55 | 203.0.113.55 | ipv4 | true | true | 해당 IP 203.0.113.55 로 비콘 트래픽 발생 | load | 2026-04-02T09:10 |
| ioc_003 | rpt_01 | 999.1.1.1 | — | ipv4 | false | true | 출처 999.1.1.1 | reject | 2026-04-02T09:20 |
| ioc_004 | rpt_02 | e3b0c44298fc1c149afbf4c8996fb92427ae41… | e3b0c44298fc1c149afbf4c8996fb92427ae41… | sha256 | true | false | — | reject | 2026-04-02T10:00 |
| ioc_005 | rpt_02 | hxxp://drop .evil-host .net/x | http://drop.evil-host.net/x | domain | true | true | 페이로드는 hxxp://drop .evil-host .net/x 에서… | load | 2026-04-02T10:20 |
아래 Zod 스키마에 맞는 IOC 추출 60행 JSON 배열로 생성합니다. 리포트 4~5개, IOC 타입 분포, 정상 위주에 거름·검토를 섞습니다. label 규칙: format_valid=false 또는 grounded=false → reject, ioc_type=unknown 또는 normalized_value=null(정규화 실패) → review, 그 외(형식·근거·정규화 모두 정상) → load.
z.object({ ioc_id: z.string(), report_id: z.string(), raw_value: z.string(), normalized_value: z.string().nullable(), ioc_type: z.enum(["domain","ipv4","sha256","md5","cve","email","unknown"]), format_valid: z.boolean(), grounded: z.boolean(), source_span: z.string().nullable(), label: z.enum(["load","review","reject"]), ts: z.string() })
쉬운 데이터만 만들지 말 것. 다음 하드 케이스를 의도적으로 섞어라(쉬운 추출만 통과시키는 함정 회피): (1) defang·표기 흔들림(같은 도메인의 'evil[.]com'/'EVIL[.]COM'/'hxxp://evil .com' 처럼 normalized_value 로 같은 값이 돼야 함) 3건 이상, (2) 형식 위반: 옥텟이 255 초과인 IPv4·63자짜리 SHA256·31자짜리 MD5 등 format_valid=false 2건 이상, (3) 원문 미근거: grounded=false, source_span=null 인 지어낸 지표 2건, (4) 미등록 타입: ioc_type=unknown 2건(review), (5) 경계값: 정규화 실패(normalized_value=null)로 review 되는 케이스 1~2건. label 은 위 규칙대로 정직하게. 진짜 스키마 위반(타입 깨짐)은 데이터가 아니라 별도 '형식 깨뜨리기 시도 3건' 지시로만 재현. JSON 배열만.z.object({ ioc_id: z.string(), report_id: z.string(), raw_value: z.string(), normalized_value: z.string().nullable(), ioc_type: z.enum(["domain","ipv4","sha256","md5","cve","email","unknown"]), format_valid: z.boolean(), grounded: z.boolean(), source_span: z.string().nullable(), label: z.enum(["load","review","reject"]), ts: z.string() })| Python | IOC 추출 파서·정규식 형식 검증·원문 근거 매칭·API 를 구현하는 언어입니다(FastAPI·asyncio·Pydantic) |
| FastAPI | 리포트 업로드에서 추출·검증·적재까지 처리하는 게이트 API 서버입니다 |
| LLM | 리포트 문서에서 IOC·관계 후보를 뽑고 근거 span 을 인용하는 추출 엔진입니다 |
| PostgreSQL | 적재된 구조화 IOC 자산과 판정 로그를 저장하는 주 저장소입니다 |
| MongoDB | 원본 비정형·반정형 위협 리포트 문서를 그대로 담는 저장소입니다 |
| Elasticsearch | 적재된 IOC 와 리포트 전문을 색인해 검색·조회하는 인덱스입니다 |
| Kafka | 신규 리포트 유입 이벤트를 흘려보내 추출 파이프라인을 트리거하는 스트림입니다(3주차) |
| Spark | 도메인·IOC 타입별 적재 정합성과 거름 사유 분포를 대량 집계하는 배치입니다(3주차) |
| Airflow | 수집에서 추출·검증·적재까지 리포트 파이프라인을 오케스트레이션합니다(3주차) |
| Kubernetes | 게이트 API 와 추출 워커 컨테이너를 배포·스케일하는 실행 환경입니다 |
| Docker | 추출·검증 서비스를 컨테이너로 패키징합니다 |
| Java | 대용량 리포트 수집을 담당하는 기존 Java 배치와 붙는 연동 어댑터입니다 |
| Claude | IOC·관계 추출과 근거 span 인용을 맡는 LLM 입니다 |
✓ pass → 그대로 멈춰도 완결입니다. 더 키우려면 2주차(적재가 맞았는지 확인)로 진행합니다.
✓ pass → 여기서 멈춰도 완결입니다. 더 키우면 3주차(리포트 파이프라인 + 타입별 정합성 추이)로 진행합니다.
IOC 1건의 적재 결정 카드. 결정·요인·근거를 한 스키마로 담아 화면 카드에 씁니다. 악성 여부 판정이 아니라 형식·근거 정합성 게이트입니다.
z.object({ ioc_id: z.string(), report_id: z.string(), decision: z.enum(["load","review","reject"]), factors: z.array(z.enum(["format_invalid","ungrounded","unnormalized","unknown_type","ambiguous_defang"])), reason: z.string().min(20), evidence: z.string().min(8), hedge: z.string().nullable() })[
{
"input_ref": "ioc_003 (IPv4 형식 위반)",
"output": {
"ioc_id": "ioc_003",
"report_id": "rpt_01",
"decision": "reject",
"factors": [
"format_invalid"
],
"reason": "999.1.1.1 은 옥텟이 255 를 넘어 IPv4 정규식을 통과하지 못합니다. 형식이 깨진 값은 구조화 자산에 적재하지 않고 거릅니다.",
"evidence": "raw_value=999.1.1.1, format_valid=false",
"hedge": "형식이 유효하지 않아 적재하지 않습니다"
}
},
{
"input_ref": "ioc_004 (원문 미근거)",
"output": {
"ioc_id": "ioc_004",
"report_id": "rpt_02",
"decision": "reject",
"factors": [
"ungrounded"
],
"reason": "해당 SHA256 값이 리포트 원문에서 근거 span 으로 확인되지 않아 LLM 이 지어냈을 수 있습니다. 근거 없는 지표는 자산에 적재하지 않습니다.",
"evidence": "grounded=false, source_span=null",
"hedge": "원문 근거가 없어 적재하지 않습니다"
}
},
{
"input_ref": "ioc_009 (미등록 타입)",
"output": {
"ioc_id": "ioc_009",
"report_id": "rpt_03",
"decision": "review",
"factors": [
"unknown_type",
"unnormalized"
],
"reason": "TA-0xNova 는 등록된 IOC 타입 정규식에 맞지 않아 형식으로 확정할 수 없습니다. 위협 행위자명으로 보이나 자동 판정 대상이 아니라 사람 검토로 돌립니다.",
"evidence": "ioc_type=unknown, normalized_value=null",
"hedge": "타입 미등록이라 적재하지 않고 검토로 둡니다"
}
}
][자동 검증] 1. Zod parse 로 출력 형식(필드·enum)이 스키마를 통과하는지 확인합니다. 2. decision 이 형식·근거 규칙(형식 위반/원문 미근거→reject·미등록 타입/정규화 실패→review·통과→load)과 일치하는지 봅니다. 3. evidence 가 입력 IOC 의 실제 값·플래그 인용인지 토큰 매칭으로 확인하고, grounded=false 면 load 가 되지 않게 강제합니다. 4. decision 을 심은 정답 label 과 비교해 정밀도·재현율을 계산합니다. [수동 검증] 1. 검토자 2명이 비동기로 적재를 라벨링해 정밀도·재현율과 bootstrap Cohen's κ(두 사람 일치가 우연 이상인 정도, 0~1) 하한 0.6 이상을 봅니다. 2. 전체의 5% 를 사람이 직접 점검합니다.