Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 32 additions & 15 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,52 +14,69 @@ cd charcnn-kit
## 파이프라인 (순서)
```bash
pip install -r requirements.txt
python src/download_phiusiil.py # 데이터 다운로드 (UCI PhiUSIIL, 5만 균형 샘플)
python src/preprocess.py # raw -> train/valid/test (8:1:1 stratified)
python src/download_train_mixed.py # 데이터 다운로드 (PhiUSIIL + URLhaus + OpenPhish + PhishTank + Tranco 혼합)
python src/preprocess.py # raw -> train/valid/test (domain-group 8:1:1 split)
python src/train.py # 학습 (early stopping + best-model 저장)
python src/evaluate.py # 지표 + FN/FP URL 출력
python src/download_ood.py # 학습 URL 제외 OOD 평가셋 생성
python src/evaluate_ood.py # OOD 지표 + threshold sweep + hard example 저장
python src/predict.py --url "..." [--xai] # 단일 URL 예측
```
대안 데이터 소스: `python src/download_data.py` (URLhaus + Tranco). 두 소스 형식 차이로 모델이 trivially 분리되므로 baseline용으론 부적합하다.
대안 데이터 소스: `python src/download_phiusiil.py` (UCI PhiUSIIL만 사용), `python src/download_data.py` (URLhaus + Tranco만 사용). 단일 출처/두 출처만 쓰면 source bias가 생기기 쉬우므로 현재 기본은 혼합 학습셋이다. PhishTank app key가 있으면 `PHISHTANK_APP_KEY` 환경변수 사용.

## 절대 어겨선 안 되는 컨벤션

- **라벨 규칙**: `0 = 정상`, `1 = 악성`. README에서도 명시한 고정 규칙. PhiUSIIL은 반대 규칙(1=legit, 0=phish)이라 `download_phiusiil.py`에서 라벨을 반전시킨다 — 이 반전 로직은 깨면 안 됨.
- **수정 금지 영역** (README 8번):
- `dataset.py`의 문자 인코딩 구조 (`build_vocab`, `encode_url`, PAD/UNK 토큰)
- `model.py`의 CharCNN 핵심 구조
- `model.py`의 `CharCNN` 핵심 구조 (CharCNN 클래스는 그대로 두고 새 클래스 `HybridCharCNN`이 sub-module만 재사용)
- 라벨 기준 자체
- **vocab 생애주기**: vocab은 `train.py`에서 train.csv로부터 생성되어 `saved/char_vocab.json`에 저장된다. evaluate/predict는 **반드시 저장된 vocab을 로드**해야 한다 (새로 만들면 character→index 매핑이 달라져 모델이 깨짐).
- **feature 정규화 통계 생애주기**: tabular feature mean/std는 `train.py`에서 train.csv로만 fit하여 `saved/feature_norm.json`에 저장. valid/test/predict는 반드시 저장된 통계를 로드해 적용 — train 외 데이터의 통계를 섞으면 data leakage가 됨.
- **금지 feature**: `URLSimilarityIndex`는 PhiUSIIL에서 라벨과 |corr|=0.86으로 cheat feature 역할. `FEATURE_COLS`에 절대 추가하지 말 것 (이슈 #3 ablation에서 확인).

## 아키텍처 (큰 그림)

**데이터 흐름:**
**데이터 흐름 (이슈 #3 이후):**
```
data/raw/urls.csv (url, label)
└─ preprocess.py → stratified 8/1/1 split, 라벨 검증, 중복/null 제거
data/raw/urls.csv (url, label, source)
└─ preprocess.py → domain-group 8/1/1 split, 라벨 검증, 중복/feature-NaN 제거
data/processed/{train,valid,test}.csv
└─ URLDataset (dataset.py) → char-level encode_url(URL) → tensor
└─ URLDataset (dataset.py) → (char-encoded URL, tabular feature 벡터, label)
→ train의 mean/std로 표준화
torch.DataLoader
└─ CharCNN (model.py): Embedding → 여러 Conv1d kernel → max-pool over time → concat → Dropout → FC(1) → BCEWithLogitsLoss
saved/charcnn.pt + saved/char_vocab.json
└─ HybridCharCNN (model.py):
├─ char 분기: Embedding → 여러 Conv1d kernel → max-pool over time → concat → Dropout
├─ tabular 분기: Linear → ReLU → Dropout (작은 MLP)
└─ 두 분기 concat → FC(1) → BCEWithLogitsLoss
saved/charcnn.pt + saved/char_vocab.json + saved/feature_norm.json
```

**CharCNN 구조 핵심:** 문자별 임베딩을 Conv1d 여러 kernel size(`KERNEL_SIZES`)로 통과시켜 각각의 max-pool 결과를 concat. URL 길이는 `MAX_LEN`(200)으로 패딩/truncate.
**CharCNN 구조 핵심:** 문자별 임베딩을 Conv1d 여러 kernel size(`KERNEL_SIZES`)로 통과시켜 각각의 max-pool 결과를 concat. URL 길이는 `MAX_LEN`(200)으로 패딩/truncate. CharCNN 클래스 자체는 보존되고, `HybridCharCNN`이 그 sub-module(embedding/convs/dropout)을 재사용해 forward를 새로 구성한다.

**Tabular feature (이슈 #3):**
- `FEATURE_COLS = URL_FEATURE_COLS` (25개). URL 문자열에서 즉시 계산 가능한 feature만 학습/평가/추론 전부에서 동일하게 사용한다.
- 동적 URL 대응 feature 포함: query/path/token 관련 `NoOfQueryParams`, `QueryLength`, `PathLength`, `NoOfPathSegments`, `HasFragment`, `NoOfEqualsInURL`, `NoOfAmpersandInURL` 등.
- `HTML_FEATURE_COLS`는 현재 비활성화. 실제 predict/evaluate_ood에서 페이지 fetch 없이 mean imputation만 하게 되면 OOD 오탐이 커지므로 다시 추가하지 말 것.

**학습 흐름 (`train.py`):**
- valid_loss를 epoch마다 계산
- `valid_loss`가 갱신될 때마다 best state를 `copy.deepcopy`로 보관
- `PATIENCE` epoch 동안 개선 없으면 early stopping
- 최종 저장은 **마지막 epoch이 아닌 best epoch의 state** (이게 핵심)
- tabular feature의 mean/std도 train에서만 fit해 `saved/feature_norm.json`에 저장

**Config (`config.py`):** 모든 하이퍼파라미터/경로의 단일 출처. README 9번에 추천 튜닝 순서가 정리되어 있음.
**Config (`config.py`):** 모든 하이퍼파라미터/경로/feature 컬럼 목록의 단일 출처. README 9번에 추천 튜닝 순서가 정리되어 있음.
**Split/Augmentation/Hard mining:** `USE_DOMAIN_GROUP_SPLIT=True`가 기본. 같은 등록 도메인이 train/valid/test에 섞이지 않도록 `StratifiedGroupKFold`를 사용한다. random row split으로 되돌리면 성능은 올라가 보여도 데이터 누수 가능성이 커진다. `download_train_mixed.py`는 정상/악성 각각 5,000개의 동적 URL(query/path/token) synthetic augmentation을 추가한다. `evaluate_ood.py`는 FN/FP를 `data/hard_examples/hard_examples.csv`에 저장하고, 다음 `download_train_mixed.py` 실행 때 클래스별 최대 `HARD_EXAMPLES_MAX_PER_CLASS`개만 재학습에 섞는다. hard example 도메인 group은 train에 고정한다.

## 알려진 제약

- **CharCNN의 본질적 한계**: URL 문자열만 보므로 "평범해 보이는 신규 phishing 도메인"은 구분 불가. 현재 baseline의 FN 9건이 모두 이 패턴(`https://www.{도메인}.{tld}` 단순 형태). recall을 더 올리려면 WHOIS/DNS feature 통합이 필요 — 모델 구조 튜닝만으로는 한계.
- **CharCNN/URL-feature의 한계**: URL 문자열만 보면 "평범해 보이는 신규 phishing 도메인"은 놓칠 수 있다. 그래서 URLhaus/Tranco/PhishTank/OpenPhish 등 외부 출처 OOD 평가를 계속 돌려야 한다.
- **PhiUSIIL HTML feature의 OOD 위험**: `URLSimilarityIndex`는 라벨과 사실상 1:1 → 학습용 `FEATURE_COLS`에서 제외. `HasSocialNet`, `HasCopyrightInfo`, `HasDescription`도 실제 추론에서 계산하지 않으면 train/test 분포가 어긋난다. 현재는 HTML feature 비활성화 상태.
- **Windows 콘솔에서 한글 mojibake**: print의 한글이 깨져 보일 수 있음(예: `악성` → `��`). 코드/저장 파일은 UTF-8로 정상이며, **콘솔 표시 문제일 뿐**이므로 무시해도 된다.
- **재현 가능 산출물은 .gitignore됨**: `data/raw/*.csv`, `data/processed/*.csv`, `saved/*.pt`, `saved/*.json`. 새 환경에서 작업할 땐 위 파이프라인을 처음부터 돌려야 한다.

## 현재 baseline (참고용)
- PhiUSIIL test 5,000건: Accuracy 0.9982, Precision 1.0000, Recall 0.9964, FN 9, FP 0
- README 목표(recall ≥0.90, accuracy ~0.90)는 충족 상태. 다음 작업의 비교 기준점.
- **Mixed + URL-feature HybridCharCNN** (2026-04-28, domain split, dynamic URL augmentation, hard-mining 1회, PhiUSIIL+URLhaus+OpenPhish+PhishTank+Tranco): internal test Accuracy 0.9975, Precision 0.9980, Recall 0.9969, FN 17, FP 11 at threshold 0.2.
- **OOD(URLhaus+PhishTank/Tranco, 학습 URL 제외)**: Accuracy 0.9975, Precision 0.9988, Recall 0.9962, FN 19, FP 6 at threshold 0.2. Hard mining은 오탐 안정성을 개선했지만 FN은 증가했으므로 recall 우선 운영이면 threshold 0.1 후보도 함께 비교할 것.
- README 목표(recall ≥0.90, accuracy ~0.90)는 둘 다 충족.
5 changes: 5 additions & 0 deletions charcnn-kit/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ __pycache__/
# 생성물: 다운로드 스크립트로 재생성 가능
data/raw/*.csv
data/processed/*.csv
data/ood/*.csv
data/hard_examples/*.csv
data/phiusiil_columns_report.txt
!data/raw/.gitkeep
!data/processed/.gitkeep
!data/ood/.gitkeep
!data/hard_examples/.gitkeep

# 학습 산출물: train.py로 재생성 가능
saved/*.pt
Expand Down
132 changes: 120 additions & 12 deletions charcnn-kit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@ http://paypa1-login.xyz,1
* `valid.csv`
* `test.csv`

#### data/ood/

외부 출처 OOD 평가 데이터 위치

* `ood_test.csv`

#### data/hard_examples/

OOD 평가에서 틀린 FN/FP URL을 누적 저장하는 위치

* `hard_examples.csv`

---

### saved/
Expand All @@ -73,6 +85,10 @@ http://paypa1-login.xyz,1

문자를 숫자로 바꾸는 사전 파일

#### feature_norm.json

tabular feature의 train mean/std 저장 파일

---

### src/
Expand All @@ -87,10 +103,14 @@ http://paypa1-login.xyz,1
| model.py | CharCNN 모델 구조 |
| train.py | 모델 학습 (early stopping + best-model 저장) |
| evaluate.py | 성능 평가 + FN/FP 사례 출력 |
| evaluate_ood.py | 외부 출처 OOD 평가 + threshold sweep |
| predict.py | URL 단일 예측 |
| explain.py | XAI 로그 기능 |
| features.py | URL feature 계산 + feature 정규화 저장/로드 |
| download_phiusiil.py | UCI PhiUSIIL 데이터셋 다운로드 (추천) |
| download_data.py | URLhaus + Tranco 데이터 다운로드 (대안) |
| download_train_mixed.py | PhiUSIIL + URLhaus + OpenPhish + PhishTank + Tranco 혼합 학습셋 생성 |
| download_ood.py | URLhaus + PhishTank + OpenPhish + Tranco OOD 평가셋 생성 |

---

Expand Down Expand Up @@ -121,7 +141,24 @@ http://paypa1-login.xyz,1
* `0` = 정상
* `1` = 악성

#### 옵션 A. 공개 데이터셋 자동 다운로드 (추천)
#### 옵션 A. 혼합 학습셋 자동 다운로드 (추천)

PhiUSIIL, URLhaus, OpenPhish, PhishTank, Tranco를 섞어 학습 데이터를 만듭니다.
현재 기본 학습 파이프라인은 이 방식을 권장합니다.

```bash
python src/download_train_mixed.py
```

기본 구성:

* 악성 약 50,000개: PhiUSIIL phishing 25,000 + URLhaus recent 15,000 + PhishTank 10,000 + OpenPhish available feed
* 정상 약 50,000개: PhiUSIIL legitimate + Tranco top 100,000 중 샘플
* 동적 URL augmentation 10,000개: 정상/악성 각각 5,000개 query/path/token 변형
* 실행 결과: `data/raw/urls.csv`
* PhishTank app key가 있으면 환경변수 `PHISHTANK_APP_KEY`에 넣으면 됩니다. 없으면 public feed를 시도합니다.

#### 옵션 B. PhiUSIIL만 사용

UCI ML PhiUSIIL Phishing URL Dataset (235k URL)에서 5만개 균형 샘플링:

Expand All @@ -133,10 +170,17 @@ python src/download_phiusiil.py

> 다른 소스도 가능: `python src/download_data.py` (URLhaus + Tranco). 다만 두 소스 형식 차이로 모델이 trivially 분리되어 baseline용으론 비추천.

#### 옵션 B. 직접 작성
#### 옵션 C. 직접 작성

CSV 형식에 맞춰 `data/raw/urls.csv`를 수동으로 채워도 됩니다.

#### 추가로 찾아오면 좋은 데이터 출처

* 악성 URL: URLhaus `https://urlhaus.abuse.ch/`, PhishTank `https://www.phishtank.org/developer_info.php`, OpenPhish `https://openphish.com/phishing_feeds.html`
* 정상 URL: Tranco `https://tranco-list.eu/`, 회사/학교/정부/뉴스/쇼핑/포털 등 실제 정상 사이트 목록
* 직접 가져온 CSV는 최소 `url,label` 컬럼을 맞추면 됩니다. 라벨은 `0=정상`, `1=악성`입니다.
* 정상 후보는 악성 feed와 겹치는 URL을 제거해야 합니다.

---

### 3단계. 전처리
Expand All @@ -153,6 +197,9 @@ data/processed/valid.csv
data/processed/test.csv
```

기본 전처리는 같은 등록 도메인이 train/valid/test에 동시에 들어가지 않도록 domain group split을 사용합니다. 이 설정은 과적합과 데이터 누수를 줄이기 위한 것입니다.
URL feature는 query/path/token 같은 동적 URL 대응을 위해 `NoOfQueryParams`, `QueryLength`, `PathLength`, `NoOfPathSegments`, `HasFragment` 등을 포함합니다.

---

### 4단계. 학습
Expand All @@ -166,6 +213,7 @@ python src/train.py
```text
saved/charcnn.pt
saved/char_vocab.json
saved/feature_norm.json
```

예상 로그:
Expand Down Expand Up @@ -195,26 +243,86 @@ python src/evaluate.py

```text
=== Evaluation Result ===
Accuracy : 0.9982
Precision: 1.0000
Recall : 0.9964
F1 Score : 0.9982
Accuracy : 0.9975
Precision: 0.9980
Recall : 0.9969
F1 Score : 0.9975
Confusion Matrix:
[[2500 0]
[ 9 2489]]
[[5509 11]
[ 17 5504]]

=== False Negatives (미탐: 악성을 정상으로 판단) [9건] ===
score=0.2706 https://www.vmailmessage.com
=== False Negatives (미탐: 악성을 정상으로 판단) [17건] ===
score=0.1295 https://verificahype.simply.site
...

=== False Positives (오탐: 정상을 악성으로 판단) [0건] ===
=== False Positives (오탐: 정상을 악성으로 판단) [11건] ===
```

* 지표 외에 **FN/FP 사례가 score와 함께 출력**되어 모델이 어떤 URL을 놓쳤는지 분석 가능합니다.

---

### 6단계. 단일 URL 예측
### 6단계. 외부 출처 OOD 평가 + hard example 저장

PhiUSIIL 내부 split 성능은 실제 인터넷 URL 성능을 보장하지 않습니다.
외부 데이터로 별도 평가합니다.

```bash
python src/download_ood.py
python src/evaluate_ood.py
```

기본 설정:

* 악성: URLhaus recent 5,000개
* 정상: Tranco 상위 100,000 도메인 중 5,000개 샘플
* 저장 위치: `data/ood/ood_test.csv`
* `download_ood.py`는 기본적으로 `data/raw/urls.csv`에 들어간 학습 URL을 제외하고 OOD 평가셋을 만듭니다.
* 현재 모델은 실제 추론에서 계산 가능한 URL 기반 feature만 사용합니다.
* `evaluate_ood.py`는 기본적으로 오분류 FN/FP를 `data/hard_examples/hard_examples.csv`에 누적 저장합니다.

hard example을 다음 학습에 반영하는 루프:

```bash
python src/evaluate_ood.py --save-hard
python src/download_train_mixed.py
python src/preprocess.py
python src/train.py
python src/download_ood.py
python src/evaluate_ood.py
```

과적합 방지:

* hard example은 클래스별 최대 `HARD_EXAMPLES_MAX_PER_CLASS`개만 학습에 섞습니다.
* hard example의 등록 도메인 group은 train에 고정하고 valid/test와 겹치지 않게 합니다.
* 같은 OOD 파일에 대해 무한 반복하지 말고, 최신 feed로 새 OOD를 만든 뒤 반복합니다.

2026-04-28 현재 HybridCharCNN OOD 결과:

```text
Threshold 0.20
Accuracy : 0.9975
Precision: 0.9988
Recall : 0.9962
F1 Score : 0.9975
Confusion Matrix [[TN FP], [FN TP]]:
[[ 4994 6]
[ 19 4981]]
```

해석:

* 이전 PhiUSIIL+HTML-feature 모델은 Tranco 정상 5,000개 중 4,981개를 오탐했습니다.
* 혼합 학습셋 + URL 기반 feature-only 모델로 바꾼 뒤 대량 오탐 문제는 해소됐습니다.
* 현재 OOD 악성은 URLhaus + PhishTank 잔여 URL을 포함합니다. OOD FN은 PhishTank 쪽에서 발생합니다.
* 동적 URL augmentation 후 OOD는 이전 FN 14 / FP 23에서 FN 12 / FP 15로 개선됐습니다.
* hard example 1회 재학습 후 OOD 오탐은 15개에서 6개로 줄었고, 내부 test도 FP 17개에서 11개로 줄었습니다. 대신 OOD FN은 12개에서 19개로 늘어 threshold/정책 선택이 필요합니다.
* OpenPhish community feed는 현재 샘플 수가 작아 추가 검증용으로는 부족합니다. 직접 수집 정상 URL과 최신 phishing feed를 계속 추가해야 합니다.

---

### 7단계. 단일 URL 예측

```bash
python src/predict.py --url "http://paypa1-login.xyz"
Expand Down
1 change: 1 addition & 0 deletions charcnn-kit/data/hard_examples/.gitkeep
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

1 change: 1 addition & 0 deletions charcnn-kit/data/ood/.gitkeep
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

67 changes: 65 additions & 2 deletions charcnn-kit/src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"

# pridict
THRESHOLD = 0.5 # score가 0.5 이상이면 악성으로 판단
THRESHOLD = 0.2 # score가 0.2 이상이면 악성으로 판단 (FN/FP 균형)

VOCAB_PATH = "saved/char_vocab.json" # 문자 사전 저장 경로
MODEL_PATH = "saved/charcnn.pt" # 모델 저장 경로
Expand All @@ -30,4 +30,67 @@
TRAIN_RATIO = 0.8 # 학습 데이터 비율
VALID_RATIO = 0.1 # 검증 데이터 비율
TEST_RATIO = 0.1 # 평가 데이터 비율
SEED = 42 # 재현성을 위한 random seed
SEED = 42 # 재현성을 위한 random seed

# === OOD 평가 ===
OOD_CSV_PATH = "data/ood/ood_test.csv" # 외부 출처 OOD 평가 데이터
OOD_SAMPLE_PER_CLASS = 5000 # OOD 평가셋 클래스별 샘플 수
OOD_BENIGN_TOP_N = 100000 # Tranco 정상 후보를 상위 N개 도메인으로 제한

# === 혼합 학습셋 / hard example 재학습 ===
HARD_EXAMPLES_PATH = "data/hard_examples/hard_examples.csv" # OOD 오분류 누적 저장
MIXED_PHIUSIIL_PER_CLASS = 25000 # 혼합 학습셋 PhiUSIIL 클래스별 샘플 수
MIXED_URLHAUS_SAMPLES = 15000 # 혼합 학습셋 URLhaus 악성 샘플 수
MIXED_OPENPHISH_SAMPLES = 5000 # 혼합 학습셋 OpenPhish 악성 샘플 수
MIXED_PHISHTANK_SAMPLES = 10000 # 혼합 학습셋 PhishTank 악성 샘플 수
MIXED_TRANCO_SAMPLES = 40000 # 혼합 학습셋 Tranco 정상 샘플 수
MIXED_TRANCO_TOP_N = 100000 # 혼합 학습용 Tranco 정상 후보 상위 N개
DYNAMIC_AUG_PER_CLASS = 5000 # 동적 URL(query/path/token) synthetic augmentation 수
HARD_EXAMPLES_MAX_PER_CLASS = 2000 # 재학습에 섞을 hard example 클래스별 최대 수
USE_DOMAIN_GROUP_SPLIT = True # 같은 등록 도메인이 train/valid/test에 섞이지 않게 분할
FORCE_HARD_EXAMPLES_TO_TRAIN = True # hard example 도메인 group은 train에 고정

# === Tabular feature 통합 (이슈 #3) ===
# URL 문자열만으로 즉시 계산 가능 → 학습/평가/추론 모두에서 사용
URL_FEATURE_COLS = [
"IsHTTPS",
"URLLength",
"DomainLength",
"NoOfSubDomain",
"IsDomainIP",
"TLDLength",
"NoOfLettersInURL",
"LetterRatioInURL",
"NoOfDegitsInURL", # PhiUSIIL 원본 오타 그대로
"DegitRatioInURL",
"SpacialCharRatioInURL",
"NoOfOtherSpecialCharsInURL",
"HasObfuscation",
"NoOfEqualsInURL",
"NoOfQMarkInURL",
"NoOfAmpersandInURL",
"NoOfAtInURL",
"NoOfDashInURL",
"NoOfDotInURL",
"NoOfPercentInURL",
"PathLength",
"QueryLength",
"NoOfPathSegments",
"NoOfQueryParams",
"HasFragment",
]
# HTML/페이지 분석이 필요 → 학습/평가만 PhiUSIIL 사전계산값 사용,
# 단일 URL 추론에서는 train mean으로 imputation
# 주의: URLSimilarityIndex는 라벨과 |corr|=0.86으로 cheat-feature이라 제외함
# (이슈 #3 ablation 결과; 다시 추가하지 말 것)
HTML_FEATURE_COLS = [
"HasSocialNet",
"HasCopyrightInfo",
"HasDescription",
]
# 현재 배포/외부 평가에서는 HTML을 실제 fetch하지 않으므로 URL 기반 feature만 사용한다.
# HTML feature를 다시 쓰려면 predict/evaluate_ood에서도 동일 feature를 계산해야 한다.
FEATURE_COLS = URL_FEATURE_COLS

FEATURE_NORM_PATH = "saved/feature_norm.json" # 학습 데이터의 mean/std 저장
TABULAR_HIDDEN = 32 # tabular 분기의 hidden 차원
Loading