Inspiration
Most phishing detectors give users a verdict without showing how they reached it, and few test whether their own rules survive real evasion techniques. PhishLens is an explainable email threat analyzer that scores every signal transparently, distinguishes “couldn’t determine” from “checked and clean,” and attacks its own production detector through a built-in Red-Team Lab.
Inspired by cybersec, this project aims to expand my knowledge of nextjs, typescript, tailwind and python frameworks. The idea came after I saw a funny video targeting an email scammer/being amazed at how elaborate they get these days with detection evasion techniques. This was also done to test how well my local agentic llm could respond to my inquiries. I partially trained it on this project. I never participated in a hackathon so I wanted to try myself.
What it does
Paste a .eml formatted email, or manually type the email in and the detector ranks it 0-100 where every point is attributable. Doesnt give a straight answer, but a suggestion based on the technical analysis. Points system scores bigger threats with more points.
On top of the content scoring, the header forensics on uploaded .eml files - the receiving mail servers own SPF, DKIM, and DMARC verdicts, computed at delivery time, plus Return-Path and Reply-to mismatches.
- Safe link tracing, which follows a redirect chain server-side to the real endpoint, so the users browser doesnt have to open it.
- Domain intelligence - WHOIS age, live SPF/DMARC lookups, and Unicode confusables detection, for pasted messages that have no headers to read.
- Incident reports, a dashboard which is not really useful at this stage, and web hook alerts on high/critical scans.
The Cherry on the cake is the Red-Team Lab, which is essentially PhishLens attacking itself. It applies real evasion techniques one at a time, invisible zero width character injection, urgency paraphrasing, sender neutralization, link laundering - keeping whichever one drops the score the most, and shows you the descending list of the steps. Then it turns every successful evasion into a hardening recommendation and rescores the fully evaded email against the hardened detector, proving which attacks it already defeats and which are fundamental limitations of the messages contents.
How we built it
This was build from my previous knowledge of TypeScript and Python and learning something new along the way(always). Also utilizing Nextjs 16 with the AppRouter and Tailwind for the front. Also used a FastApi service in Py for anoything that needs a network call. No database which would be considered a limitation, but the data is a JSON store.
At the core of it is a concept completely new to me, which is a heuristic engine written purely with functions. An email goes in, a scored list of named indicators comes out - no classes, no state and no I/O. This single decision was the driving power for the whole architecture. It made the engine unit-testable and most importantly, it meant for the red lab that it could attack the exact function the product itself uses instead of a copy. The attack results are decided by points, so the system is very concrete in my opinion.
(Table for a more straightforward version of this information)
| Layer | What it adds |
|---|---|
| Header forensics | SPF/DKIM/DMARC verdicts and spoofing mismatches from raw .eml |
| Link tracer | Real redirect destination, resolved server-side |
| Python enrichment | WHOIS age, live SPF/DMARC, full-table Unicode confusables |
| Claude layer | Plain-English explanation for a non-technical recipient |
Two challenges fundamentally shaped my python service. Fist, a single WHOIS lookup can take as long as 3 seconds, which is suboptimal since Node caller gives up at exactly 4 - so all five checks run simultaneously, each with its own timeout, under a 3.2 seconds exactly budget. Second, every check fails closed to "Couldn't determine" which is essentially a different value from the "Checked and Clean".
For the whole project I applied a rule: every external dependency is optional to the process. The Ai layer, the Py service and the Slack web hook - each has its own timeout and degrades to "the app works regardless, and tells you what is unavailable." The repo does not require API keys.
I used my local server to deploy claude into the project and ran it on my custom package in VScode to test how well it works and also to help me with code here and there since Im working alone on a very tight timeline and could realistically spend 30/40 hours on this before the deadline. Hence why im uploading it last second. And im still bad at Tailwind.
Challenges we ran into
Invisible characters broke the build countless times. The zero width unicode ended up in adversary.ts and in a commit message - while building a tool that detects invisible unicode. Had to finds them with od -c and convert to escapes.
A stale in memory cache 404s real scans Incident reports failed for scans that existed on disk, because Next gives the API route and the page separate module instances - each with its own cache. Invisible in every test; only surfaces by clicking the actual link.
False positives, not detection were the hardest challenge(I go over them in the YouTube video) -The DKIM check docked points from google.com since google uses custom DKIM selectors, so probing the common ones found nothing. -The typo check flagged the real github newsletter as impersonation of GitLab - two character edit difference. -Mixed script detection testing the whole hostname at once, so any non Latin domain on a .com looked suspicious to the software.
Accomplishments that we're proud of
The Red Team Lab is my favorite child so far
The engine is not an animation and uses an actual evasion operator against the live score and keeps one only if it actually lowers the score. It can legitimately fail to evade the email if its written masterfully. On the demo sample it drives a critical email from 93 to 0 in only five steps, then re-scores the fully evaded result against the detector to show which attacks already died.
Building the service changed the direction of the project multiple times. Unicode normalization exists in the matcher today because the adversary exploited the absence of it. The feature that tests the tool improved the tool. That's the part I would point out.
Whole-script confusables detection that beats the library its built on. Verified against 7 attack domains and 18 legitimate ones - including Cyrillic and Chinese IDNs with no false positives in either direction.
I hunted down all the false positives by hand and can name every single one.
In the video and README, im honest about the limitations of the project which came difficult to me; someplaces, the heuristics break, why header forensics need the raw .eml, why the DKIM signal is informational rather than just scared.
What we learned
Adversarial testing isn't a QA phase but a design tool. I built the Red-Team Lab expecting a demo feature but went way further than expected with it. The Lab ended up rewriting the matcher and justifying three more layers of the product. Anything that finds your blind spots is automatically a change in what youre building.
"Couldn't determine" and "checked and clean" are very different values, and collapsing them is how most of the security tools get by lying to the customer. My python is focused heavily on keeping a clear line between both.
Unicode is way more complex than I anticipated. I had a very wrong expectation of unicode and its importance in this project; mixed-script versus the whole-script confusables, puny code, zero width injection, normalization - went in thinking homograph detection was a regex and came out having read most of unicodes security specs.
Directing the LLM well is being conscious about what you need to reject. Its fast and good and it will confidently give you a DKIM check that penalizes google.com. the value wasn't in generating the code, it as in having enough judgement to test the output realistically and throw some parts away. A lot of very interesting fixes came out of this part.
What's next for PhishLens
Semantic evasion operator in the Lab - using an LLM to paraphrase instead of a substitution table. A much harder adversary to test the ceiling of such detection.
Auto-hardening mode - the lab proposes matcher changes itself and shows before/after detection rates across a corpus, closing the loop of finding the weakness to actually having fixed it.
Real inbox generation (Gmail/outlook add ins) so this runs where the emails actually travel to, removing the need to copy paste.
Trained classifier in the Python layer - the response contract already has the field but for now returns null
Shared database if this solution actually gets its spotlight. Also a rate limiter would be nice. These are two things that actually close the gap between a dev model and actual online deployment.
Ivan
Built With
- fastapi
- json
- next.js
- python
- tailwind
- typescript
Log in or sign up for Devpost to join the conversation.