π‘️ PhishGuard – Real-Time URL Threat Analyzer | Detect Phishing Links Instantly
Demo :
Click Video πππ
Content:
PhishGuard – Your Personal Cyber Bodyguard!
In today’s digital world, one wrong click can cost you your data, money, or even identity.
That’s why we built PhishGuard, a modern, fast, and accurate URL Threat Analyzer built with Python + Tkinter.
π Key Features:
-
Detect suspicious TLDs and malicious patterns
-
Spot phishing tricks like
@symbols, misleading subdomains, Unicode risks -
Analyze HTTPS status, redirect hops, and Punycode
-
Export full security reports to CSV
-
Modern dark/light UI design
Code :
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import csv, re, ssl, socket, time, unicodedata
from urllib.parse import urlparse
from urllib.request import Request, urlopen
import idna
APP_NAME = "PhishGuard – URL Threat Analyzer"
VERSION = "1.0.0"
SUS_TLDS_DEFAULT = {
"zip","mov","xyz","click","country","gq","top","work","link","fit","kim","men",
"loan","download","science","cricket","date","faith","racing","review","party"
}
def is_ip(host: str) -> bool:
return bool(re.fullmatch(r"(?:\d{1,3}\.){3}\d{1,3}", host))
def has_at_symbol(url: str) -> bool:
return "@" in url
def excessive_length(url: str) -> bool:
return len(url) > 120
def count_subdomains(host: str) -> int:
return host.count(".") - 1 if "." in host else 0
def looks_like_confusable(host: str) -> bool:
# flag if non-ASCII or contains mixed scripts
try:
host.encode("ascii")
return False
except:
pass
# Mixed-script heuristic (simplified)
cats = set(unicodedata.name(c).split()[0] for c in host if ord(c) > 127)
return len(cats) > 1
def has_punycode(host: str) -> bool:
return "xn--" in host
def extract_tld(host: str) -> str:
parts = host.split(".")
return parts[-1].lower() if len(parts) > 1 else ""
def has_misleading_subdomain(host: str) -> bool:
# e.g., login.paypal.com.security-team.org
keywords = ["secure", "security", "verify", "update", "login", "auth", "support"]
pats = [rf"{k}.*\..*\..*" for k in keywords]
return any(re.search(p, host, re.I) for p in pats)
def uses_https(parsed) -> bool:
return parsed.scheme.lower() == "https"
def normalized_host(host: str) -> str:
try:
return idna.encode(host).decode()
except Exception:
return host
def head_redirects(url: str, timeout=4, max_hops=4):
# lightweight HEAD to count redirects (without requests)
try:
ctx = ssl.create_default_context()
req = Request(url, method="HEAD", headers={"User-Agent":"Mozilla/5.0"})
hops, final_url = 0, url
while hops < max_hops:
with urlopen(req, context=ctx, timeout=timeout) as resp:
code = resp.getcode()
if code in (301,302,303,307,308) and resp.getheader("Location"):
final_url = resp.getheader("Location")
req = Request(final_url, method="HEAD", headers={"User-Agent":"Mozilla/5.0"})
hops += 1
else:
break
return hops, final_url, None
except Exception as e:
return 0, url, str(e)
def score_url(url: str, sus_tlds=SUS_TLDS_DEFAULT):
# Normalize
if not re.match(r"^https?://", url, re.I):
url = "http://" + url # allow raw host input
parsed = urlparse(url)
if not parsed.netloc:
return {"error":"Invalid URL"}, 0
host = parsed.netloc.lower()
host_norm = normalized_host(host)
features = {
"uses_https": uses_https(parsed),
"has_at_symbol": has_at_symbol(url),
"is_ip_host": is_ip(host.split(":")[0]),
"excessive_length": excessive_length(url),
"subdomain_count": max(count_subdomains(host_norm), 0),
"has_punycode": has_punycode(host_norm),
"confusable": looks_like_confusable(host_norm),
"misleading_subdomain": has_misleading_subdomain(host_norm),
"suspicious_tld": extract_tld(host_norm) in sus_tlds
}
# Redirect check (non-blocking feel)
redirects, final_url, err = head_redirects(url)
features["redirect_hops"] = redirects
features["head_error"] = err
# Scoring (0 safe -> 100 risky)
score = 0
# positive signals
if features["uses_https"]:
score -= 5
# negatives
score += 25 if features["has_at_symbol"] else 0
score += 20 if features["is_ip_host"] else 0
score += 15 if features["excessive_length"] else 0
score += min(20, features["subdomain_count"] * 6)
score += 20 if features["has_punycode"] else 0
score += 20 if features["confusable"] else 0
score += 18 if features["misleading_subdomain"] else 0
score += 12 if features["suspicious_tld"] else 0
score += min(15, features["redirect_hops"] * 5)
score = max(0, min(100, score))
label = ("SAFE", "#10b981") if score < 25 else ("SUSPICIOUS", "#f59e0b") if score < 65 else ("PHISHING RISK", "#ef4444")
return {
"input_url": url,
"final_url": final_url,
"features": features,
"score": score,
"label": label[0],
"color": label[1]
}, score
class App(ttk.Frame):
def __init__(self, master):
super().__init__(master, padding=16)
self.master.title(f"{APP_NAME} • v{VERSION}")
self.master.geometry("960x560")
self.master.minsize(860, 520)
self.dark = True
self.style = ttk.Style()
self._apply_theme()
self._build_ui()
def _apply_theme(self):
base = "clam"
self.style.theme_use(base)
bg = "#0f172a" if self.dark else "#f7fafc"
fg = "#e2e8f0" if self.dark else "#1f2937"
card = "#111827" if self.dark else "#ffffff"
accent = "#22d3ee"
danger = "#ef4444"
self.master.configure(bg=bg)
self.style.configure("TFrame", background=bg)
self.style.configure("Card.TFrame", background=card, relief="flat")
self.style.configure("TLabel", background=bg, foreground=fg, font=("Segoe UI", 11))
self.style.configure("Card.TLabel", background=card, foreground=fg, font=("Segoe UI", 11))
self.style.configure("TButton", font=("Segoe UI Semibold", 10))
self.style.configure("Accent.TButton", foreground="#0b1020", font=("Segoe UI Semibold", 11))
self.style.map("Accent.TButton", background=[("!disabled", "#22d3ee")])
self.style.configure("Score.TLabel", font=("Segoe UI Black", 32))
self.style.configure("Title.TLabel", font=("Segoe UI Black", 18))
self.style.configure("Hint.TLabel", foreground="#94a3b8", font=("Segoe UI", 10))
def _build_ui(self):
header = ttk.Frame(self, style="TFrame")
header.pack(fill="x")
title = ttk.Label(header, text="π‘️ PhishGuard", style="Title.TLabel")
title.pack(side="left", pady=6)
ttk.Button(header, text="π Theme", command=self.toggle_theme).pack(side="right", padx=6)
ttk.Button(header, text="π Save CSV", command=self.save_csv).pack(side="right", padx=6)
# Card
card = ttk.Frame(self, style="Card.TFrame", padding=16)
card.pack(fill="both", expand=True, pady=12)
self.url_var = tk.StringVar()
ttk.Label(card, text="Enter URL to Analyze", style="Card.TLabel").pack(anchor="w")
top = ttk.Frame(card, style="Card.TFrame")
top.pack(fill="x", pady=8)
self.entry = ttk.Entry(top, textvariable=self.url_var, font=("Segoe UI", 12))
self.entry.pack(side="left", fill="x", expand=True, ipady=6)
ttk.Button(top, text="Analyze", style="Accent.TButton", command=self.analyze).pack(side="left", padx=8)
ttk.Button(top, text="Paste", command=self.paste_clip).pack(side="left")
ttk.Button(top, text="Copy Report", command=self.copy_report).pack(side="left", padx=6)
# Result area
self.score_lbl = ttk.Label(card, text="—", style="Score.TLabel")
self.score_lbl.pack(pady=(8,0))
self.badge = ttk.Label(card, text="", font=("Segoe UI Bold", 12))
self.badge.pack()
self.tree = ttk.Treeview(card, columns=("feature","value"), show="headings", height=9)
self.tree.heading("feature", text="Feature")
self.tree.heading("value", text="Value")
self.tree.column("feature", width=280, anchor="w")
self.tree.column("value", width=560, anchor="w")
self.tree.pack(fill="both", expand=True, pady=(10,0))
ttk.Label(card, text="Tip: HTTPS alone ≠ safe. Look for excessive subdomains, redirects, and suspicious TLDs.", style="Hint.TLabel").pack(anchor="w", pady=(8,0))
self.pack(fill="both", expand=True)
self.recent_results = [] # for CSV
def toggle_theme(self):
self.dark = not self.dark
self._apply_theme()
def paste_clip(self):
try:
self.url_var.set(self.master.clipboard_get())
except:
pass
def analyze(self):
url = self.url_var.get().strip()
if not url:
messagebox.showwarning("Empty", "Please enter a URL.")
return
self.tree.delete(*self.tree.get_children())
self.score_lbl.configure(text="Analyzing…")
self.badge.configure(text="", foreground="#e2e8f0")
self.master.update_idletasks()
result, _ = score_url(url)
if "error" in result:
messagebox.showerror("Invalid URL", result["error"])
self.score_lbl.configure(text="—")
return
score = result["score"]
color = result["color"]
label = result["label"]
self.score_lbl.configure(text=f"Risk Score: {score}/100", foreground=color)
self.badge.configure(text=label, foreground=color)
feats = result["features"]
rows = [
("Input URL", result["input_url"]),
("Final URL (after redirects)", result["final_url"]),
("HTTPS", str(feats["uses_https"])),
("Has @ symbol", str(feats["has_at_symbol"])),
("IP as host", str(feats["is_ip_host"])),
("Excessive length", str(feats["excessive_length"])),
("Subdomain count", str(feats["subdomain_count"])),
("Punycode (xn--)", str(feats["has_punycode"])),
("Confusable/Unicode risk", str(feats["confusable"])),
("Misleading subdomain", str(feats["misleading_subdomain"])),
("Suspicious TLD", str(feats["suspicious_tld"])),
("Redirect hops (HEAD)", str(feats["redirect_hops"])),
("HEAD error", str(feats["head_error"])),
]
for r in rows:
self.tree.insert("", "end", values=r)
# record for CSV
rec = {
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
"input_url": result["input_url"],
"final_url": result["final_url"],
"score": score,
"label": label
}
rec.update({k:str(v) for k,v in feats.items()})
self.recent_results.append(rec)
def copy_report(self):
sel = [self.tree.item(i)["values"] for i in self.tree.get_children()]
text = "\n".join(f"{k}: {v}" for k,v in sel)
self.master.clipboard_clear()
self.master.clipboard_append(text)
messagebox.showinfo("Copied", "Report copied to clipboard.")
def save_csv(self):
if not self.recent_results:
messagebox.showwarning("No Data", "Analyze at least one URL first.")
return
path = filedialog.asksaveasfilename(defaultextension=".csv", initialfile="phishguard_report.csv")
if not path:
return
keys = list(self.recent_results[0].keys())
with open(path, "w", newline="", encoding="utf-8") as f:
w = csv.DictWriter(f, fieldnames=keys)
w.writeheader()
for r in self.recent_results:
w.writerow(r)
messagebox.showinfo("Saved", f"Saved: {path}")
def main():
root = tk.Tk()
App(root)
root.mainloop()
if __name__ == "__main__":
main()
Comments
Post a Comment