faizhuda commited on
Commit
3c83173
·
verified ·
1 Parent(s): 5b9dbb5

Publish phishing v2.1

Browse files
v2.1/config.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "activation": "gelu",
3
+ "architectures": [
4
+ "DistilBertForSequenceClassification"
5
+ ],
6
+ "attention_dropout": 0.1,
7
+ "bos_token_id": null,
8
+ "dim": 768,
9
+ "dropout": 0.1,
10
+ "dtype": "float32",
11
+ "eos_token_id": null,
12
+ "hidden_dim": 3072,
13
+ "id2label": {
14
+ "0": "legit",
15
+ "1": "phishing"
16
+ },
17
+ "initializer_range": 0.02,
18
+ "label2id": {
19
+ "legit": 0,
20
+ "phishing": 1
21
+ },
22
+ "max_position_embeddings": 512,
23
+ "model_type": "distilbert",
24
+ "n_heads": 12,
25
+ "n_layers": 6,
26
+ "pad_token_id": 0,
27
+ "qa_dropout": 0.1,
28
+ "seq_classif_dropout": 0.2,
29
+ "sinusoidal_pos_embds": false,
30
+ "tie_weights_": true,
31
+ "tie_word_embeddings": true,
32
+ "transformers_version": "5.0.0",
33
+ "use_cache": false,
34
+ "vocab_size": 30522
35
+ }
v2.1/eval_report.json ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "checkpoint": "checkpoints/phishing_v2",
3
+ "test_split": "data/processed/phishing/test",
4
+ "metrics": {
5
+ "recall_phishing": 0.9903169014084507,
6
+ "precision_phishing": 0.9774109470026064,
7
+ "f1_phishing": 0.9838216003498033,
8
+ "confusion_matrix": [
9
+ [
10
+ 1339,
11
+ 26
12
+ ],
13
+ [
14
+ 11,
15
+ 1125
16
+ ]
17
+ ],
18
+ "threshold": 0.06,
19
+ "n_test": 2501,
20
+ "n_phishing_test": 1136,
21
+ "n_legit_test": 1365,
22
+ "p95_ms": 242.0971423996889,
23
+ "latency": {
24
+ "mean_ms": 100.66626771500523,
25
+ "p50_ms": 59.67643049984872,
26
+ "p95_ms": 242.0971423996889,
27
+ "n": 200.0
28
+ }
29
+ },
30
+ "gates": {
31
+ "recall": true,
32
+ "precision": true,
33
+ "latency_p95": true
34
+ },
35
+ "all_passed": true
36
+ }
v2.1/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7692afc2685e34b5736a8af98aa889929c6c4a7ce2850adfce009d32e82ecfc5
3
+ size 267832560
v2.1/model_card.md ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ language:
3
+ - en
4
+ license: other
5
+ tags:
6
+ - text-classification
7
+ - phishing-detection
8
+ - distilbert
9
+ base_model: distilbert-base-uncased
10
+ pipeline_tag: text-classification
11
+ ---
12
+
13
+ # Model Card — PriorMail Phishing Detector v2.1
14
+
15
+ **Model ID:** `phishing/v2.1`
16
+ **Owner:** Faiz (PJK-GM095)
17
+ **Date:** 2026-06-19
18
+ **Git SHA:** `7cdeae7` (branch `feat/phishing-distilbert-v2`)
19
+ **Status:** In-distribution §8 gates **PASS**; real-world acceptance gate **FAILS on false-negative rate** (documented known-limitation below). Ship decision is deliberate, scoped, and pending sign-off from Insan + Syafiq — this is **not** an automatic promotion.
20
+
21
+ ---
22
+
23
+ ## What This Model Does
24
+
25
+ Binary email classifier: **legit (0)** vs **phishing (1)**.
26
+
27
+ Used by `prior-mail-backend` to flag potentially malicious emails. Intended as **one layer** of defense — see Known Limitations for why it should not be the sole line.
28
+
29
+ ---
30
+
31
+ ## Base Model
32
+
33
+ `distilbert-base-uncased` (~67M params).
34
+
35
+ Replaces v1.0's `bert-base-multilingual-cased`: the training data is English-only, and DistilBERT is ~3× smaller and faster on CPU (the backend runs CPU on Render), easing the 500 ms latency gate and the checkpoint-size constraint.
36
+
37
+ ---
38
+
39
+ ## Training Data
40
+
41
+ | Source | Role | Notes |
42
+ |---|---|---|
43
+ | `ealvaradob/phishing-dataset` (`texts`) | Phishing + benign legit | ~20,137 rows; ~62% benign |
44
+ | `insanar/prior-mail-priority` (`v4`) | Legit negatives | Product-distribution legit mail (transactional/banking) |
45
+ | Synthetic augmentation (`src/data/augment.py`) | Both classes | 600 rows/class, header-complete, seed 42 |
46
+
47
+ Enron (`emails.csv`) is **off** in v2 (it made v1 learn "non-Enron-style ⇒ phishing" → over-flagging). Legit pool downsampled to ~1.2× phishing; deduplicated on the first 200 chars of body before splitting to prevent leakage.
48
+
49
+ **After dedup:** ~16,669 rows (7,572 phishing / 9,097 legit)
50
+ **Split (stratified, seed 42):** train 11,667 / val 2,501 / test 2,501
51
+
52
+ The synthetic augmentation covers tactics the public corpora miss (BEC/CEO-fraud, fake invoice, brand- and IT-helpdesk credential harvest, delivery). It is **header-complete on both classes** so header *presence* cannot become a class proxy (the v1 leak).
53
+
54
+ ---
55
+
56
+ ## Input / Output Format
57
+
58
+ **Input** (`src/data/preprocess.build_phishing_input()`):
59
+ ```
60
+ {sender display-name + domain} [SEP] {subject} [SEP] {body}
61
+ ```
62
+ Text is cleaned with `keep_domains=True`: URL **host** and email **domain** are kept as signal (lookalike domains, odd TLDs) while the PII-bearing URL path/query and email local-part are masked. The `[SEP]` scaffolding is always present (header-less rows carry empty fields) — this is what makes the sender/subject reinstatement leak-safe.
63
+
64
+ **Output:**
65
+ - Softmax probability for class 1 (phishing), range [0, 1].
66
+ - Label `phishing` if probability ≥ threshold, else `legit`.
67
+ - **Threshold: 0.06**, stored in `threshold.json`. Selected on the validation set to maximize F-beta (β=2, recall-weighted — false negatives are the worst case) subject to precision ≥ 0.80; median of the optimal plateau.
68
+
69
+ ---
70
+
71
+ ## Training Config
72
+
73
+ See [`configs/phishing_v2.yaml`](../configs/phishing_v2.yaml). Key hyperparameters:
74
+
75
+ | Parameter | Value |
76
+ |---|---|
77
+ | Learning rate | 2e-5 (AdamW) |
78
+ | Batch size | 16 per device |
79
+ | Max sequence length | 512 |
80
+ | Epochs | 4 (early stopping, patience 2) |
81
+ | Early stopping metric | `f1_phishing` |
82
+ | Loss | Weighted cross-entropy, `phishing_class_multiplier=1.0` (threshold drives recall, not the loss) |
83
+ | Mixed precision | bf16 |
84
+ | Seed | 42 |
85
+
86
+ ---
87
+
88
+ ## Evaluation Results
89
+
90
+ ### In-distribution test set (n=2,501) — §8 corpus gates
91
+
92
+ | Metric | Result | Gate | Status |
93
+ |---|---|---|---|
94
+ | Recall | **0.990** | ≥ 0.95 | PASS ✅ |
95
+ | Precision | **0.977** | ≥ 0.80 | PASS ✅ |
96
+ | Latency p95 (CPU) | **242 ms** | < 500 ms | PASS ✅ |
97
+ | F1 | 0.984 | — | — |
98
+
99
+ Confusion matrix (test):
100
+
101
+ | | Predicted Legit | Predicted Phishing |
102
+ |---|---|---|
103
+ | **Actual Legit** | 1,339 (TN) | 26 (FP) |
104
+ | **Actual Phishing** | 11 (FN) | 1,125 (TP) |
105
+
106
+ > The test split is drawn from the **same corpora** as training, so it measures in-distribution fit, not real-world behaviour (v1.0 passed these yet over-flagged real mail). The acceptance set below is the real-world judge.
107
+
108
+ ### Real-world acceptance set (36 hand-curated `.eml`, 18 legit / 18 phishing)
109
+
110
+ | Metric | Result | Gate | Status |
111
+ |---|---|---|---|
112
+ | False-positive rate | **0.00** (0/18) | ≤ 0.20 | PASS ✅ |
113
+ | False-negative rate | **0.33** (6/18) | ≤ 0.05 | **FAIL ❌** |
114
+ | Accuracy | 0.833 | — | — |
115
+
116
+ - **Zero false positives** — including deliberately "scary but legit" mail (a real new-sign-in security alert, bank statement, password-reset notice, CI/Slack/GitHub notifications). The v1/v2 over-flagging problem is resolved.
117
+ - **Caught (12/18):** credential harvest (PayPal, IT-helpdesk), BEC/CEO-fraud, fake invoice, bank/card suspension, lottery, package delivery, tax refund, crypto wallet, job-offer scam, voicemail lure.
118
+ - **Missed (6/18), all scored ≈0.00:** phishing that **mimics a routine SaaS/workflow notification** — DocuSign signature request, MFA-reset notice, payroll direct-deposit redirect, shared-document lure, subscription-renewal scare, and an Office 365 lookalike-domain login.
119
+
120
+ ---
121
+
122
+ ## Known Limitations
123
+
124
+ - **Workflow-mimicking phishing (primary gap).** The model learned "phishing = urgent threat + suspicious URL." Phishing that impersonates a *calm, routine* notification (DocuSign/MFA/payroll/shared-document/subscription) whose only tell is a lookalike sender domain is scored confidently legit. This is the **ceiling of synthetic-template augmentation** — the model reliably catches tactics it was trained on and misses novel ones. Closing it requires **diverse real phishing data** (e.g. Nazario / PhishTank email corpora), not more synthetic templates, and/or a **header/auth-signal layer (SPF/DKIM/DMARC, display-name↔domain mismatch) at the backend** — which is the right home for sender-spoofing detection.
125
+ - **Acceptance FN gate fails (0.33 > 0.05).** Per CLAUDE.md §8/§12 this is *not* a clean promotion. It is shipped as a scoped candidate with this limitation documented; do **not** treat the model as a sole line of defense.
126
+ - **English only.** Indonesian-language phishing is not represented and not validated.
127
+ - **Adversarial robustness** (obfuscated URLs, Unicode/homoglyph tricks beyond simple lookalikes) is not systematically evaluated.
128
+
129
+ ---
130
+
131
+ ## Artifacts
132
+
133
+ | File | Description |
134
+ |---|---|
135
+ | `model.safetensors` | Fine-tuned weights (~256 MB) |
136
+ | `config.json` | HuggingFace model config (`id2label`: 0=legit, 1=phishing) |
137
+ | `tokenizer.json`, `tokenizer_config.json`, `vocab.txt` | Tokenizer (distilbert-base-uncased) |
138
+ | `threshold.json` | `{"threshold": 0.06}` |
139
+ | `training_config.yaml` | Full training config + git SHA + timestamp |
140
+ | `eval_report.json` | In-distribution test metrics + confusion matrix + latency |
141
+ | `acceptance_report.json` | 36-file real-world acceptance results (per-file table) |
142
+ | `val_metrics.json` | Validation metrics at best epoch |
143
+
144
+ ---
145
+
146
+ ## Changelog
147
+
148
+ | Version | Date | Notes |
149
+ |---|---|---|
150
+ | v1.0 | 2026-06-13 | Initial release. mBERT. Passed corpus gates; over-flagged real mail at inference. |
151
+ | v2.1 | 2026-06-19 | DistilBERT rework. Domain-aware cleaning (A1), header-complete synthetic augmentation + sender/subject input (B1/B2), FN-averse threshold selection, real-world `.eml` acceptance gate. Corpus gates pass, FP eliminated (0%); documented FN gap on workflow-mimicking phishing. |
152
+
153
+ > Reproduce: `make data-phishing && make train-phishing config=configs/phishing_v2.yaml && make eval-phishing config=configs/phishing_v2.yaml` then `python -m src.eval.acceptance_phishing --config configs/phishing_v2.yaml` (git `7cdeae7`, seed 42).
v2.1/threshold.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "threshold": 0.06
3
+ }
v2.1/tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
v2.1/tokenizer_config.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "backend": "tokenizers",
3
+ "cls_token": "[CLS]",
4
+ "do_lower_case": true,
5
+ "is_local": false,
6
+ "mask_token": "[MASK]",
7
+ "model_max_length": 512,
8
+ "pad_token": "[PAD]",
9
+ "sep_token": "[SEP]",
10
+ "strip_accents": null,
11
+ "tokenize_chinese_chars": true,
12
+ "tokenizer_class": "BertTokenizer",
13
+ "unk_token": "[UNK]"
14
+ }
v2.1/training_config.yaml ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Phishing detector — v2 config (DistilBERT rework).
2
+ #
3
+ # v2.1 update (2026-06-19): the v2 run passed the §8 in-distribution gates but
4
+ # missed half the real .eml acceptance phishing (FN rate 0.50). Fixes landed in
5
+ # the data/preprocess layer (no hyperparameter change needed):
6
+ # A1 — clean_text(keep_domains=True): URL host + email domain kept as signal
7
+ # (lookalike domains / odd TLDs) instead of an opaque [URL].
8
+ # B1 — synthetic augmentation: prepare adds 600 rows/class (header-complete,
9
+ # both classes) covering BEC / fake-invoice / brand- AND IT-helpdesk
10
+ # credential tactics the corpora miss. Tune via --augmentation-size.
11
+ # B2 — input is now {sender_domain} [SEP] {subject} [SEP] {body} (was body-only);
12
+ # leak-safe because the [SEP] scaffolding is always present and the
13
+ # augmentation gives BOTH classes realistic headers.
14
+ # Real-world promotion is now also gated on the .eml acceptance set
15
+ # (src/eval/acceptance_phishing.py), not just the in-distribution §8 metrics.
16
+ #
17
+ # Why v2 (see feat/phishing-distilbert-v2 analysis, 2026-06-18):
18
+ # v1.0 passed test gates (recall 0.970 / precision 0.928) but over-flagged on
19
+ # real .eml at inference. Root causes addressed here:
20
+ # #1 Base model : distilbert-base-uncased — on-spec (ML_PIPELINE.md §3, mBERT
21
+ # dropped: data is English-only; DistilBERT is faster on CPU
22
+ # and ~3× smaller, easing the 500 ms gate + checkpoint size).
23
+ # #4 Class weight : multiplier dropped 3.0 -> 1.0. v1 stacked balanced (inverse-
24
+ # frequency) AND a 3× multiplier => ~6× gradient pressure on
25
+ # phishing, saturating its probability for almost any input.
26
+ # Pure inverse-frequency now; the THRESHOLD does the recall
27
+ # work, not the loss.
28
+ # #5 Early stop : monitor f1_phishing, not recall_phishing. Monitoring raw
29
+ # recall picked the most trigger-happy checkpoint; F1 balances
30
+ # recall against precision. Recall is still guaranteed by the
31
+ # eval-time threshold + gate.
32
+ # (#2 legit diversity + #3 body-only input are in loaders.py / preprocess.py.)
33
+ #
34
+ # Run via:
35
+ # make data-phishing
36
+ # make train-phishing config=configs/phishing_v2.yaml
37
+
38
+ model_type: phishing
39
+ model_name: distilbert-base-uncased # §3 decision: English-only data, on-spec
40
+
41
+ dataset: ealvaradob/phishing-dataset # primary phishing training data (HF)
42
+ legit_source: emails.csv # Enron corpus — negative class source
43
+
44
+ num_labels: 2 # legit (0) | phishing (1) — matches PHISHING_LABELS in constants.py
45
+
46
+ hyperparameters:
47
+ learning_rate: 2.0e-5 # AdamW (mirrors priority protocol, ML_PIPELINE.md §3)
48
+ weight_decay: 0.01
49
+ batch_size: 16
50
+ gradient_accumulation_steps: 1
51
+ num_epochs: 4 # 3–5 with early stopping
52
+ warmup_ratio: 0.10
53
+ lr_scheduler_type: linear
54
+ max_seq_length: 512
55
+ # Early stopping on F1 — balances recall vs precision (fix #5). Recall is still
56
+ # enforced by the post-training threshold + the >=0.95 eval gate.
57
+ early_stopping_metric: f1_phishing
58
+ early_stopping_patience: 2
59
+ class_weights: balanced # inverse-frequency base weights from train dist
60
+ phishing_class_multiplier: 1.0 # fix #4: no extra stacking — let threshold drive recall
61
+ mixed_precision: bf16 # falls back to fp16 / none as needed
62
+
63
+ seed: 42
64
+
65
+ output_dir: checkpoints/phishing_v2
66
+
67
+ wandb:
68
+ project: priormail
69
+ tags:
70
+ - model:phishing
71
+ - stage:exp
72
+
73
+ # --- run provenance (auto-recorded) ---
74
+ _git_sha: 7cdeae7
75
+ _git_dirty: False
76
+ _trained_at: 2026-06-19T03:00:44