{"id":51591644,"url":"https://github.com/trac3r00/phish-triage","last_synced_at":"2026-07-11T15:01:58.443Z","repository":{"id":370023358,"uuid":"1265322232","full_name":"trac3r00/phish-triage","owner":"trac3r00","description":"Phishing email triage toolkit: .eml parsing, IOC enrichment, detection rules (SPL + Sigma)","archived":false,"fork":false,"pushed_at":"2026-07-07T23:25:42.000Z","size":686,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-08T01:11:30.768Z","etag":null,"topics":["detection-engineering","email-security","ioc","mitre-attack","phishing","sigma","soc","splunk"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/trac3r00.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-06-10T17:06:28.000Z","updated_at":"2026-07-07T23:25:46.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/trac3r00/phish-triage","commit_stats":null,"previous_names":["trac3r00/phish-triage"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/trac3r00/phish-triage","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/trac3r00%2Fphish-triage","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/trac3r00%2Fphish-triage/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/trac3r00%2Fphish-triage/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/trac3r00%2Fphish-triage/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/trac3r00","download_url":"https://codeload.github.com/trac3r00/phish-triage/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/trac3r00%2Fphish-triage/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35365809,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-11T02:00:05.354Z","response_time":104,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["detection-engineering","email-security","ioc","mitre-attack","phishing","sigma","soc","splunk"],"created_at":"2026-07-11T15:01:56.579Z","updated_at":"2026-07-11T15:01:58.436Z","avatar_url":"https://github.com/trac3r00.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# phish-triage\n\n[![Python](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/)\n[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)\n[![MITRE ATT\u0026CK](https://img.shields.io/badge/MITRE%20ATT%26CK-mapped-red.svg)](https://attack.mitre.org/)\n[![Detections](https://img.shields.io/badge/detections-Sigma%20%2B%20Splunk%20SPL-orange.svg)](detections/)\n[![Tests](https://img.shields.io/badge/tests-28%20passing-brightgreen.svg)](tests/)\n\nA small, batteries-included **phishing email triage toolkit** for SOC analysts\nand detection engineers.\n\nFeed it a single `.eml` file; get back:\n\n1. A **deterministic parse** of every signal that matters — SPF/DKIM/DMARC,\n   From vs Reply-To \u0026 display-name spoofing, Received-chain anomalies, URLs\n   (defanged), attachments with cryptographic hashes.\n2. **External enrichment** (VirusTotal v3 + AbuseIPDB free-tier) gated by a\n   built-in rate limiter and an on-disk cache.\n3. A **weighted 0-100 verdict** (`benign` / `suspicious` / `malicious`) and a\n   plain-language rationale.\n4. **Eight production-style detections** — Splunk SPL + Sigma — mapped to\n   MITRE ATT\u0026CK, that fire on the same signals the tool surfaces.\n\n\u003e ⚙️  Stage 1 is **pure Python 3.11 stdlib**.  Stage 2 only adds `requests`.\n\n📖 **Long-form context:** [why I built this](docs/why.md) · [retrospective](docs/retrospective.md)\n\n## Screenshots\n\nThe bundled web UI (`python -m phish_triage.web`, see\n[Sample tester](#sample-tester-web-ui) below) lets you drop in any `.eml`\nand see the verdict, signals, IOCs, and reasoning side-by-side.\n\n|   |   |\n|---|---|\n| **Home page — upload or pick a sample**\u003cbr\u003e![home page](docs/screenshots/01-home.png) | **Phish sample → MALICIOUS 100/100**\u003cbr\u003e![phish report](docs/screenshots/02-phish-report.png) |\n| **Benign sample → BENIGN 0/100 (no false positives)**\u003cbr\u003e![benign report](docs/screenshots/03-benign-report.png) | **CLI session — same toolkit, terminal-first**\u003cbr\u003e![cli session](docs/screenshots/04-cli-session.png) |\n\n## Architecture\n\n```mermaid\nflowchart TD\n    EML([\".eml\"])\n    EML --\u003e S1[\"Stage 1 · parser.py (stdlib)\u003cbr/\u003eSPF/DKIM/DMARC · From vs Reply-To\u003cbr/\u003eReceived chain · URLs · attachment hashes\"]\n    S1 --\u003e OUT1[\"JSON / markdown summary\"]\n    S1 --\u003e|ParsedEmail| S2[\"Stage 2 · enrich.py\u003cbr/\u003eVirusTotal v3 (URLs, file hashes)\u003cbr/\u003eAbuseIPDB (Rx-chain IPs)\u003cbr/\u003erate limit + cache · weighted scorer\"]\n    S2 --\u003e OUT2[\"Markdown triage report\u003cbr/\u003e(summary + IOC table + verdict)\"]\n    S2 --\u003e|EnrichmentResult| S3[\"Stage 3 · detections/\u003cbr/\u003e8 Splunk SPL queries\u003cbr/\u003e8 Sigma rules · MITRE ATT\u0026CK mapping\"]\n    S3 --\u003e OUT3[\"Drop into Splunk / SIEM\"]\n```\n\n## Quickstart\n\n```bash\ngit clone https://github.com/trac3r00/phish-triage.git\ncd phish-triage\npython3.11 -m venv .venv \u0026\u0026 source .venv/bin/activate\npip install -e \".[dev]\"\n\n# regenerate the sample fixtures (safe: no real malware)\npython -m tests.make_fixtures\n\n# Stage 1 — parse a .eml, print JSON\nphish-triage parse tests/fixtures/phish_sample.eml --json\n\n# Stage 1 — same email, markdown summary\nphish-triage parse tests/fixtures/phish_sample.eml --markdown\n\n# Stage 2 — parse + enrich (uses env vars; missing keys degrade gracefully)\nexport VT_API_KEY=\u003cyour-vt-key\u003e          # https://www.virustotal.com/gui/my-apikey\nexport ABUSEIPDB_API_KEY=\u003cyour-abuse-key\u003e   # https://www.abuseipdb.com/account/api\nphish-triage enrich tests/fixtures/phish_sample.eml --output report.md\n\n# run the test suite\npytest -q\n```\n\n## Sample tester (web UI)\n\n```bash\npip install -e \".[web]\"\npython -m phish_triage.web\n# open http://127.0.0.1:5050\n```\n\nA single-file Flask app under `src/phish_triage/web/` that lets you upload\nany `.eml` or click one of the bundled samples and see the parsed signals,\nIOCs, verdict, and rationale rendered as cards.\n\n\u003e ⚠️  The web demo runs the parser + scorer **locally only** — VirusTotal and\n\u003e AbuseIPDB are intentionally skipped so the page is safe to share without\n\u003e exposing API keys or burning quota.  Add keys back via the CLI\n\u003e (`phish-triage enrich …`) when you want the full pipeline.\n\n## Sample output\n\n```text\n$ phish-triage enrich tests/fixtures/phish_sample.eml | head -25\n# Phishing triage report — Urgent: PayPal account suspended\n\n**Source:** `tests/fixtures/phish_sample.eml`\n\n## Summary\n\n- **Verdict:** `MALICIOUS`\n- **Score:** 100 / 100\n- **From:** PayPal Security \u003csecurity-team@paypa1-verify.top\u003e\n- **Reply-To:** verify@evil-collector.country\n- **Subject:** Urgent: PayPal account suspended\n\n## IOC table\n\n| Type | Value                                            | Notes                |\n|------|--------------------------------------------------|----------------------|\n| url  | `hxxp://bit[.]ly/paypal-verify-now`              | host `bit.ly`        |\n| url  | `hxxps://paypa1-secure-login[.]zip/account`      | host `paypa1...zip`  |\n| file | `invoice.html`                                   | sha256 `1fec...f873` |\n| ip   | `198.51.100.42`                                  | from Received chain  |\n```\n\n## Stage breakdown\n\n### Stage 1 — Parser (pure stdlib)\n\n* Uses `email.policy.default` for RFC 5322 parsing — handles MIME, RFC 2047\n  header encoding, quoted-printable / base64 transfer encoding.\n* `Authentication-Results` parsed mechanism-by-mechanism; the strongest\n  signal wins (a later `none` won't overwrite an earlier `fail`).\n* `From` vs `Reply-To` mismatch and display-name brand spoofing detection.\n* `Received` chain ordered origin → recipient with **RFC 1918-only** privacy\n  detection (RFC 5737 documentation ranges, which test fixtures use as\n  stand-in public IPs, are treated as public — see the comment in\n  `_is_private`).\n* All URLs from every `text/*` body part, deduplicated and defanged\n  (`hxxp://`, `[.]`).\n* Attachments yield filename, content-type, size, and MD5/SHA1/SHA256.\n* Output: JSON or a markdown analyst summary.\n\n[Read the Stage 1 walkthrough →](docs/stage1-parsing-walkthrough.md)\n\n### Stage 2 — Enrichment (`requests` only)\n\n* VirusTotal v3 GET endpoints for URLs (`/urls/{base64id}`) and files\n  (`/files/{sha256}`), bounded by a 4 req/min RateLimiter that matches the\n  free-tier quota.\n* AbuseIPDB `/check` for every **public** Received-chain hop.\n* Missing API key? The source is skipped, a line is added to the\n  `Notes` section of the report, scoring continues with whatever's left.\n* Responses cached under `.cache/` keyed by `(source, IOC)` so re-runs don't\n  burn quota.\n* Verdict scoring: signal weights + VT detections + AbuseIPDB confidence\n  bounded 0-100, mapped to benign / suspicious / malicious at 40 / 70.\n\n[Read the Stage 2 walkthrough →](docs/stage2-enrichment-walkthrough.md)\n\n### Stage 3 — Detections (Splunk SPL + Sigma)\n\n* Eight rules in both SPL and Sigma covering every category of signal the\n  parser surfaces.\n* `detections/README.md` carries the rule ↔ parser signal ↔ MITRE ATT\u0026CK\n  table.\n\n[Read the detection-engineering write-up →](docs/stage3-detection-engineering.md)\n\n## MITRE ATT\u0026CK mapping\n\n| Rule | Technique(s) |\n|------|--------------|\n| R01 SPF fail + From/Reply-To mismatch | [T1566.001](https://attack.mitre.org/techniques/T1566/001/), [T1566.002](https://attack.mitre.org/techniques/T1566/002/) |\n| R02 New sender domain + auth fail | [T1566](https://attack.mitre.org/techniques/T1566/) |\n| R03 Brand / exec display-name spoof | [T1656](https://attack.mitre.org/techniques/T1656/), [T1566.001/002](https://attack.mitre.org/techniques/T1566/) |\n| R04 URL shortener in mail | [T1566.002](https://attack.mitre.org/techniques/T1566/002/), [T1204.001](https://attack.mitre.org/techniques/T1204/001/) |\n| R05 Attachment hash → VirusTotal | [T1566.001](https://attack.mitre.org/techniques/T1566/001/) |\n| R06 Suspicious TLD link | [T1566.002](https://attack.mitre.org/techniques/T1566/002/), [T1583.001](https://attack.mitre.org/techniques/T1583/001/) |\n| R07 Base64 HTML body with form | [T1027](https://attack.mitre.org/techniques/T1027/), [T1566.001](https://attack.mitre.org/techniques/T1566/001/) |\n| R08 DMARC reject bypass | [T1566](https://attack.mitre.org/techniques/T1566/), defense_evasion |\n\n## Project layout\n\n```\nphish-triage/\n├── src/phish_triage/\n│   ├── __init__.py\n│   ├── parser.py       Stage 1 — stdlib-only parser\n│   ├── enrich.py       Stage 2 — VT + AbuseIPDB + scoring\n│   ├── cli.py          argparse front-end (phish-triage parse|enrich)\n│   └── web/            Stage 4 — single-file Flask sample tester\n│       ├── __init__.py\n│       ├── __main__.py\n│       ├── templates/{index,report}.html\n│       └── static/style.css\n├── tests/\n│   ├── fixtures/\n│   │   ├── benign.eml          regenerated from make_fixtures.py\n│   │   └── phish_sample.eml\n│   ├── make_fixtures.py        no real malware — only signal patterns\n│   ├── test_parser.py          9 cases\n│   ├── test_enrich.py          11 cases, HTTP fully mocked\n│   └── test_web.py             8 cases — Flask test client\n├── detections/\n│   ├── spl/                     R01-R08 (.spl)\n│   ├── sigma/                   R01-R08 (.yml, uuid4 ids, MITRE tags)\n│   └── README.md                signal → rule → ATT\u0026CK mapping\n├── docs/\n│   ├── why.md                              why I built this\n│   ├── retrospective.md                    what worked, what bit me\n│   ├── stage1-parsing-walkthrough.md\n│   ├── stage2-enrichment-walkthrough.md\n│   ├── stage3-detection-engineering.md\n│   └── screenshots/                        README image assets\n└── pyproject.toml\n```\n\n## Testing\n\n```bash\npytest -q          # 28 tests, all green, no live API calls\n```\n\nThe enrichment tests use `unittest.mock.patch` against\n`phish_triage.enrich.requests.Session.get`, so the suite is **safe to run\nwithout API keys** and **never** burns quota.\n\n## Safety / scope\n\n* `tests/make_fixtures.py` deliberately constructs the phishing fixture from\n  inert text — there is **no real malware** in this repo.\n* The toolkit only performs **read-only** lookups against public APIs.\n* No `eval` / `exec` / `shell=True` anywhere; no hardcoded credentials.\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftrac3r00%2Fphish-triage","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftrac3r00%2Fphish-triage","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftrac3r00%2Fphish-triage/lists"}