πŸ›‘️ 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

Popular posts from this blog

Is This News Real or Fake? πŸ€– AI Exposes the Truth | FuzzuTech Python App Demo

🚨 Python Intrusion Detection System (IDS) – Real-Time ML + Tkinter GUI Project | FuzzuTech

πŸš€ Simple Login & Registration System in Python Tkinter πŸ“±