"""
Ne Nasedaj — SMS Phishing Detector
Bilingual version (EN/SR) with language toggle
Ensemble: TF-IDF/LogReg + fine-tuned BERTić classifier
"""
import gradio as gr
import pandas as pd
import re
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
# =========================
# LOAD DATASETS & DOMAINS
# =========================
try:
df = pd.read_csv("sms_dataset_lc.csv")
except FileNotFoundError:
df = pd.DataFrame({"text": [], "label": []})
try:
phishing_domains_df = pd.read_csv("phishing_domains.csv")
typosquatting_domains_df = pd.read_csv("typosquatting_domains.csv")
PHISHING_DOMAINS = set(phishing_domains_df['domain'].str.lower().tolist())
TYPOSQUATTING_DOMAINS = set(typosquatting_domains_df['domain'].str.lower().tolist())
ALL_SUSPICIOUS_DOMAINS = PHISHING_DOMAINS.union(TYPOSQUATTING_DOMAINS)
except Exception:
ALL_SUSPICIOUS_DOMAINS = set()
# =========================
# TF-IDF / LOGREG MODEL (Model A)
# =========================
if len(df) > 0:
tfidf_model = Pipeline([
("tfidf", TfidfVectorizer(
analyzer='char_wb',
ngram_range=(3, 5),
lowercase=True,
max_features=10000
)),
("clf", LogisticRegression(
class_weight='balanced',
max_iter=1000,
random_state=42
))
])
tfidf_model.fit(df["text"], df["label"])
else:
tfidf_model = None
# =========================
# BERTIĆ MODEL (Model B)
# =========================
class SMSPhishingDetectorWrapper:
def __init__(self, model_id="ravi2505/ne-nasedaj-sms-phishing", device=None):
self.model_id = model_id
self.device = device or torch.device("cuda" if torch.cuda.is_available() else "cpu")
self.tokenizer = AutoTokenizer.from_pretrained(model_id)
self.model = AutoModelForSequenceClassification.from_pretrained(model_id)
self.model.to(self.device)
self.model.eval()
self.id2label = {0: "Legitimate", 1: "Phishing"}
def _prepare_input(self, text):
if text and len(text) > 4 and text[0] == '[' and text[3] == ']':
return text
return f"[sr] {text}"
def predict(self, text):
processed = self._prepare_input(text)
inputs = self.tokenizer(processed, return_tensors="pt", truncation=True,
padding=True, max_length=512)
inputs = {k: v.to(self.device) for k, v in inputs.items()}
with torch.no_grad():
logits = self.model(**inputs).logits
probs = torch.softmax(logits, dim=1)
pred_id = torch.argmax(probs, dim=1).item()
return {
"label": self.id2label[pred_id],
"confidence": probs[0][pred_id].item(),
"phishing_score": probs[0][1].item(),
}
try:
bertic_model = SMSPhishingDetectorWrapper()
BERTIC_AVAILABLE = True
except Exception as e:
print(f"BERTić model unavailable: {e}")
bertic_model = None
BERTIC_AVAILABLE = False
# =========================
# TRANSLATION DICTIONARIES
# =========================
TRANSLATIONS = {
"en": {
"title": "# 🛡️ NE NASEDAJ — SMS PHISHING DETECTOR",
"subtitle": "### Ensemble AI analysis for SR/HR/BIH/CG",
"sms_input_label": "✉️ Enter SMS message",
"sms_placeholder": "Paste SMS message here...",
"human_check": "I am not a robot",
"privacy_check": "I accept the terms of use",
"terms_link": "📄 Read terms of use",
"analyze_btn": "🔍 Analyze",
"output_label": "🔎 Result",
"quick_tests": "💡 Quick tests",
"phishing_title": "🚨 Phishing examples",
"legit_title": "✅ Legitimate examples",
"batch_title": "🧪 Batch testing — upload CSV",
"batch_desc": "Upload `smishi_test_cases.csv` (columns: `text`, `label`, optional `technique`)",
"upload_csv": "📂 Upload CSV",
"run_batch": "▶️ Run batch test",
"batch_output": "📊 Batch results",
"footer": "Ne Nasedaj — protect yourself from scams 🛡️",
"lang_toggle": "🌐 Language / Jezik",
"threat_vectors": {
"IBAN_PHISHING": {"label": "💳 IBAN Phishing", "desc": "Message asks for payment to an IBAN account."},
"URGENCY": {"label": "⏰ Urgency", "desc": "Message uses time pressure or threat."},
"CREDENTIAL_HARVEST": {"label": "🔑 Credential Harvest", "desc": "Message asks for personal or login details."},
"FAKE_DELIVERY": {"label": "📦 Fake Delivery", "desc": "Impersonates package or customs notification."},
"VISHING": {"label": "📞 Vishing", "desc": "Calls to contact a fake institution by phone."},
"PRIZE_SCAM": {"label": "🎁 Prize Scam", "desc": "Fake notification about a prize or win."},
"FINANCIAL_BAIT": {"label": "💰 Financial Bait", "desc": "Promises refund or financial gain."},
"TYPOSQUATTING": {"label": "🌐 Typosquatting domain", "desc": "Link leads to fake site imitating a real institution."},
},
"len_too_short": "⚠️ Too short",
"len_suspicious": "⚠️ Suspiciously short",
"len_std": "✅ Standard length",
"len_too_long": "⚠️ Too long",
"len_too_short_desc": "Message is too short to be a real notification.",
"len_suspicious_desc": "Real notifications are usually longer.",
"len_std_desc": "Length corresponds to standard SMS.",
"len_too_long_desc": "Standard SMS is {max_len} characters.",
"status_phishing_high": "🚨 PHISHING — Do not fall for it!",
"status_models_disagree": "⚠️ MODELS DISAGREE — Be cautious",
"status_not_sure": "⚠️ NOT SURE — Be cautious",
"status_possible_fraud": "⚠️ POSSIBLE FRAUD — Verify sender",
"status_legit": "✅ LEGITIMATE — Looks safe",
"why_both_phishing": "Both models indicate phishing.",
"why_model_detected": "Model detected phishing attack patterns.",
"why_disagree_tfidf": "TF-IDF model says {pred_a}, BERTić model says {pred_b}.",
"why_low_confidence": "Models are not confident enough to make a clear decision.",
"why_suspicious_features": "Message has suspicious features, but confidence is not high.",
"why_legit": "Both models agree the message does not contain typical phishing patterns.",
"why_red_flags": "Suspicious words: {red_flags}",
"why_typosquatting": "Typosquatting domain: {domains}",
"why_note_red_flags": "⚠️ Note: there are suspicious words ({red_flags}).",
"action_phishing_high": "Do not click on links. Report the message to your operator.",
"action_disagree": "Check with the sender directly before clicking or replying.",
"action_not_sure": "Check with the sender directly before clicking or replying.",
"action_possible_fraud": "Contact the institution directly via their official number.",
"action_legit": "Verify the sender before clicking.",
"copy_btn": "📋 Copy message",
"copy_success": "✅ Copied!",
"copy_error": "❌",
"report_cert_btn": "🚨 Report to {name}",
"report_police_btn": "👮 Report to {name}",
"warn_contact_btn": "⚠️ Warn contact",
"what_to_do": "🚀 What to do:",
"why_label": "🔍 Why?",
"advice_label": "💡 Advice:",
"detected_vectors": "🏷️ Detected attack vectors:",
"ensemble_confidence": "📊 Ensemble confidence",
"model_tfidf": "🤖 TF-IDF:",
"model_bertic": "🧠 BERTić:",
"model_unavailable": "unavailable",
"length_label": "📏 Length: {length} chars",
"batch_technique": "Technique",
"batch_message": "Message",
"batch_expected": "Expected",
"batch_verdict": "Verdict",
"batch_models": "A/B models",
"batch_vectors": "Vectors",
"batch_ok": "OK?",
"batch_summary": "📊 Batch test results (ensemble)",
"batch_total": "Total:",
"batch_correct": "Correct:",
"batch_wrong": "Wrong:",
"batch_uncertain": "Uncertain:",
"batch_accuracy": "Accuracy:",
"batch_uncertain_label": "⚠️ Models disagree / Not sure",
"batch_phishing_label": "🚨 Phishing",
"batch_possible_label": "⚠️ Possible fraud",
"batch_legit_label": "✅ Legitimate",
"terms_title": "📄 Terms of Use — Ne Nasedaj",
"terms_p1": "1. Purpose
Ne Nasedaj is a free SMS phishing detection tool for users in Serbia, Croatia, Bosnia and Herzegovina, and Montenegro.",
"terms_p2": "2. Data Processing
The message text you enter is analyzed only locally within the application. Messages are not stored, logged, or sent to external servers.",
"terms_p3": "3. Limitation of Liability
Analysis results are for informational purposes only and do not constitute a guarantee. The tool may make mistakes — always use common sense and verify the sender directly.",
"terms_p4": "4. Prohibition of Misuse
Using this tool for attack testing, generating phishing content, or any other malicious activity is prohibited.",
"terms_p5": "5. Contact
For questions and bug reports, contact us via the Hugging Face repository.",
"terms_accept": "I understand and accept ✓",
"ex_phishing_1": "Posta Serbia: Your package awaits customs. Pay 450 RSD: https://rs-posta.net",
"ex_phishing_2": "Raiffeisen: Card blocked. Verify: https://raiffeisen-sigurno.com",
"ex_legit_1": "Yettel: Subscription renewed. Balance: 0.00 RSD.",
"ex_legit_2": "AIK Banka: E-token code 847392. Valid 60s.",
"warning_text": "WARNING: This SMS has been identified as a potential phishing attack. Do not click links or enter personal information.",
},
"sr": {
"title": "# 🛡️ NE NASEDAJ — SMS PHISHING DETECTOR",
"subtitle": "### Ensemble AI analiza za SR/HR/BIH/CG",
"sms_input_label": "✉️ Ukucaj SMS poruku",
"sms_placeholder": "Nalepite tekst SMS poruke ovde...",
"human_check": "Nisam robot",
"privacy_check": "Prihvatam uslove korišćenja",
"terms_link": "📄 Pročitaj uslove korišćenja",
"analyze_btn": "🔍 Analiziraj",
"output_label": "🔎 Rezultat",
"quick_tests": "💡 Brzi testovi",
"phishing_title": "🚨 Phishing primeri",
"legit_title": "✅ Legitimni primeri",
"batch_title": "🧪 Batch testiranje — učitaj CSV",
"batch_desc": "Upload `smishi_test_cases.csv` (kolone: `text`, `label`, opciono `technique`)",
"upload_csv": "📂 Učitaj CSV",
"run_batch": "▶️ Pokreni batch test",
"batch_output": "📊 Batch rezultati",
"footer": "Ne Nasedaj — zaštitite se od prevara 🛡️",
"lang_toggle": "🌐 Jezik / Language",
"threat_vectors": {
"IBAN_PHISHING": {"label": "💳 IBAN Phishing", "desc": "Poruka traži uplatu na IBAN račun."},
"URGENCY": {"label": "⏰ Urgentnost", "desc": "Poruka koristi pritisak vremena ili prijetnju."},
"CREDENTIAL_HARVEST": {"label": "🔑 Krađa podataka", "desc": "Poruka traži unos ličnih ili pristupnih podataka."},
"FAKE_DELIVERY": {"label": "📦 Lažna dostava", "desc": "Imitacija obaveštenja o paketu ili carini."},
"VISHING": {"label": "📞 Vishing", "desc": "Poziva na telefonski kontakt sa lažnom institucijom."},
"PRIZE_SCAM": {"label": "🎁 Nagradna prevara", "desc": "Lažno obaveštenje o nagradi ili dobitku."},
"FINANCIAL_BAIT": {"label": "💰 Finansijski mamac", "desc": "Obećava povrat novca ili finansijsku korist."},
"TYPOSQUATTING": {"label": "🌐 Typosquatting domen", "desc": "Link vodi na lažni sajt koji imitira pravu instituciju."},
},
"len_too_short": "⚠️ Prekratko",
"len_suspicious": "⚠️ Sumnjivo kratko",
"len_std": "✅ Standardna dužina",
"len_too_long": "⚠️ Predugo",
"len_too_short_desc": "Poruka je prekratka da bi bila realno obaveštenje.",
"len_suspicious_desc": "Realna obaveštenja su obično duža.",
"len_std_desc": "Dužina odgovara standardnom SMS-u.",
"len_too_long_desc": "Standardni SMS je {max_len} karaktera.",
"status_phishing_high": "🚨 PHISHING — Ne nasedajte!",
"status_models_disagree": "⚠️ MODELI SE NE SLAŽU — Budite oprezni",
"status_not_sure": "⚠️ NISAM SIGURAN — Budite oprezni",
"status_possible_fraud": "⚠️ MOGUĆA PREVARA — Proverite pošiljaoca",
"status_legit": "✅ LEGITIMNO — Izgleda bezbedno",
"why_both_phishing": "Oba modela ukazuju na phishing.",
"why_model_detected": "Model je detektovao obrasce phishing napada.",
"why_disagree_tfidf": "TF-IDF model kaže {pred_a}, BERTić model kaže {pred_b}.",
"why_low_confidence": "Modeli nisu dovoljno sigurni da donesu jasnu odluku.",
"why_suspicious_features": "Poruka ima sumnjive karakteristike, ali pouzdanost nije visoka.",
"why_legit": "Oba modela se slažu da poruka ne sadrži tipične phishing obrasce.",
"why_red_flags": "Sumnjive reči: {red_flags}",
"why_typosquatting": "Typosquatting domen: {domains}",
"why_note_red_flags": "⚠️ Napomena: postoje sumnjive reči ({red_flags}).",
"action_phishing_high": "Ne klikajte na linkove. Prijavite poruku operateru.",
"action_disagree": "Proverite pošiljaoca direktno pre nego što kliknete ili odgovorite.",
"action_not_sure": "Proverite pošiljaoca direktno pre nego što kliknete ili odgovorite.",
"action_possible_fraud": "Kontaktirajte instituciju direktno putem zvaničnog broja.",
"action_legit": "Proverite pošiljaoca pre klika.",
"copy_btn": "📋 Kopiraj poruku",
"copy_success": "✅ Kopirano!",
"copy_error": "❌",
"report_cert_btn": "🚨 Prijavi {name}",
"report_police_btn": "👮 Prijavi {name}",
"warn_contact_btn": "⚠️ Upozori kontakt",
"what_to_do": "🚀 Šta da radite:",
"why_label": "🔍 Zašto?",
"advice_label": "💡 Savet:",
"detected_vectors": "🏷️ Detektovani vektori napada:",
"ensemble_confidence": "📊 Pouzdanost ansambla",
"model_tfidf": "🤖 TF-IDF:",
"model_bertic": "🧠 BERTić:",
"model_unavailable": "nedostupan",
"length_label": "📏 Dužina: {length} kar.",
"batch_technique": "Tehnika",
"batch_message": "Poruka",
"batch_expected": "Očekivano",
"batch_verdict": "Verdikt",
"batch_models": "A/B modeli",
"batch_vectors": "Vektori",
"batch_ok": "OK?",
"batch_summary": "📊 Rezultati batch testa (ensemble)",
"batch_total": "Ukupno:",
"batch_correct": "Tačno:",
"batch_wrong": "Pogrešno:",
"batch_uncertain": "Nesigurno:",
"batch_accuracy": "Tačnost:",
"batch_uncertain_label": "⚠️ Modeli se ne slažu / Nisam siguran",
"batch_phishing_label": "🚨 Phishing",
"batch_possible_label": "⚠️ Moguća prevara",
"batch_legit_label": "✅ Legitimno",
"terms_title": "📄 Uslovi korišćenja — Ne Nasedaj",
"terms_p1": "1. Svrha alata
Ne Nasedaj je besplatni alat za detekciju SMS phishing poruka namenjen korisnicima u Srbiji, Hrvatskoj, Bosni i Hercegovini i Crnoj Gori.",
"terms_p2": "2. Obrada podataka
Tekst poruke koji unesete analizira se isključivo lokalno unutar aplikacije. Poruke se ne čuvaju, ne beleže i ne šalju na eksterne servere.",
"terms_p3": "3. Ograničenje odgovornosti
Rezultati analize su informativnog karaktera i ne predstavljaju garanciju. Alat može pogrešiti — uvek koristite zdrav razum i proverite pošiljaoca direktno.",
"terms_p4": "4. Zabrana zloupotrebe
Zabranjeno je korišćenje ovog alata u svrhe testiranja napada, generisanja phishing sadržaja ili bilo koje druge zlonamerne aktivnosti.",
"terms_p5": "5. Kontakt
Za pitanja i prijavu grešaka kontaktirajte nas putem Hugging Face repozitorijuma.",
"terms_accept": "Razumem i prihvatam ✓",
"ex_phishing_1": "Pošta Srbije: Vaš paket čeka na carini. Platite carinu od 450 RSD: https://rs-posta.net",
"ex_phishing_2": "Raiffeisen: Kartica blokirana. Potvrdite: https://raiffeisen-sigurno.com",
"ex_legit_1": "Yettel: Pretplata obnovljena. Stanje: 0,00 RSD.",
"ex_legit_2": "AIK Banka: E-token kod 847392. Važi 60s.",
"warning_text": "UPOZORENJE: Ova SMS poruka je identifikovana kao potencijalni phishing napad. Ne klikajte na linkove i ne unosite lične podatke.",
}
}
# =========================
# LOCALIZED THREAT VECTORS
# =========================
def get_localized_threat_vectors(lang):
tv = TRANSLATIONS[lang]["threat_vectors"]
localized = {}
for key, info in tv.items():
localized[key] = {
"label": info["label"],
"desc": info["desc"],
"color": THREAT_VECTORS_BASE[key]["color"],
"bg": THREAT_VECTORS_BASE[key]["bg"],
"keywords": THREAT_VECTORS_BASE[key]["keywords"],
}
return localized
THREAT_VECTORS_BASE = {
"IBAN_PHISHING": {"color": "#dc2626", "bg": "#fef2f2",
"keywords": ["iban", "broj računa", "prenos", "uplata na račun"]},
"URGENCY": {"color": "#d97706", "bg": "#fffbeb",
"keywords": ["hitno", "rok", "odmah", "danas", "blokiran", "isteklo", "kazna", "blokirajte"]},
"CREDENTIAL_HARVEST": {"color": "#7c3aed", "bg": "#f5f3ff",
"keywords": ["pin", "lozinka", "jmbg", "sms kod", "verifikujte", "potvrdite", "unesite"]},
"FAKE_DELIVERY": {"color": "#0891b2", "bg": "#ecfeff",
"keywords": ["paket", "carinu", "doplata", "pošta", "dostava", "pošiljalac", "preuzmite"]},
"VISHING": {"color": "#be185d", "bg": "#fdf2f8",
"keywords": ["0800", "pozovite", "nazovite", "besplatan poziv", "call center"]},
"PRIZE_SCAM": {"color": "#15803d", "bg": "#f0fdf4",
"keywords": ["nagrada", "osvojili", "besplatno", "poklon", "izabrani ste", "dobitnik"]},
"FINANCIAL_BAIT": {"color": "#b45309", "bg": "#fffbeb",
"keywords": ["refundacija", "povrat", "rata", "naknadu", "kredit", "zajam"]},
"TYPOSQUATTING": {"color": "#991b1b", "bg": "#fef2f2", "keywords": []},
}
PHISHING_KEYWORDS = [
"platite", "plati", "plaćanje", "unesite", "potvrdite", "verifikujte",
"blokirajte", "hitno", "rok", "ažurirajte", "preuzmite", "aktivirajte",
"bit.ly", ".net", ".info", ".help", ".xyz", "nagrada", "osvojili",
"besplatno", "kazna", "carinu", "doplata", "jmbg", "pin", "sms kod",
"iban", "0800", "pozovite", "nazovite", "naknadu", "refundaciju", "rata",
]
REPORT_CONTACTS = {
"sr": {"cert_name": "CERT Srbije", "cert_url": "https://www.cert.rs/rs/prijava.html",
"police_name": "MUP / OVTK", "police_url": "https://www.mup.gov.rs/wps/portal/sr/kontakt"},
"hr": {"cert_name": "CERT.hr", "cert_url": "https://www.cert.hr/oincprijavi/",
"police_name": "MUP RH", "police_url": "https://www.gov.hr/mup"},
"bs": {"cert_name": "CERT.ba", "cert_url": "https://cert.ba/bs/contact",
"police_name": "MUP BiH", "police_url": "https://www.mup.gov.ba"},
"me": {"cert_name": "CERT.me", "cert_url": "https://www.cert.me/kontakt",
"police_name": "MUP CG", "police_url": "https://www.gov.me/mpb"},
}
# =========================
# HELPER FUNCTIONS
# =========================
def get_red_flags(text):
text_lower = text.lower()
return [kw for kw in PHISHING_KEYWORDS if kw in text_lower]
def get_threat_vectors(text, lang):
text_lower = text.lower()
tv = get_localized_threat_vectors(lang)
detected = []
for key, info in tv.items():
if key == "TYPOSQUATTING":
continue
if any(kw in text_lower for kw in info["keywords"]):
detected.append(key)
return detected
def extract_domains(text):
url_pattern = r'https?://[^\s<>"{}|^`\[\]]+|www\.[^\s<>"{}|\\^`\[\]]+'
urls = re.findall(url_pattern, text.lower())
domains = []
for url in urls:
domain = url.replace('http://', '').replace('https://', '').replace('www.', '')
domain = domain.split('/')[0].split('?')[0]
domains.append(domain)
return domains
def check_typosquatting(text):
domains = extract_domains(text)
return [d for d in domains if d in ALL_SUSPICIOUS_DOMAINS]
def analyze_sms_length(text, lang):
length = len(text)
gsm_chars = set('@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ !"#¤%&\'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà')
is_gsm = all(c in gsm_chars for c in text)
max_length = 160 if is_gsm else 70
t = TRANSLATIONS[lang]
if length < 20:
return length, t["len_too_short"], "#ef4444", t["len_too_short_desc"]
elif length < 60:
return length, t["len_suspicious"], "#f59e0b", t["len_suspicious_desc"]
elif length > max_length * 2:
return length, t["len_too_long"], "#f59e0b", t["len_too_long_desc"].format(max_len=max_length)
return length, t["len_std"], "#10b981", t["len_std_desc"]
def infer_language(t):
t_lower = t.lower()
if any(w in t_lower for w in ["hrvatska", "hpb", "fina", "pošta hrvatska"]):
return "hr"
if any(w in t_lower for w in ["bosna", "fbih", "bih", "bosanski", "pošta bih"]):
return "bs"
if any(w in t_lower for w in ["crna gora", "podgorica", "telekom cg"]):
return "me"
if re.search(r'[\u0400-\u04FF]', t):
return "sr"
return "sr"
# =========================
# RENDER HELPERS
# =========================
def render_confidence_bar(confidence, status_color, lang):
t = TRANSLATIONS[lang]
bar_width = max(0, min(100, confidence)) * 2.4
return f"""
{t['what_to_do']}
{status_line}
{t["detected_vectors"]}
{flag_chips}{len_desc}
{t['why_label']}
{"
".join(why_parts)}
{t['advice_label']}
{action}
{action_buttons}Upload a CSV file.
" if lang == "en" else "Učitajte CSV fajl.
" try: df_test = pd.read_csv(file.name) except Exception as e: return f"Error: {e}
" if not {'text', 'label'}.issubset(set(df_test.columns)): return f"CSV must have columns: text and label
" if lang == "en" else "CSV mora imati kolone: text i label
" THRESHOLD_HIGH, THRESHOLD_LOW = 80, 60 results, correct, uncertain, total = [], 0, 0, len(df_test) for _, row in df_test.iterrows(): text = str(row.get('text', '')).strip() expected = str(row.get('label', '')).strip().lower() technique = str(row.get('technique', '—')) if not text: continue lang_key = infer_language(text) if tfidf_model is not None: pred_a = tfidf_model.predict([text])[0] conf_a = round(max(tfidf_model.predict_proba([text])[0]) * 100, 2) else: pred_a, conf_a = "legitimate", 50.0 if BERTIC_AVAILABLE: bertic_result = bertic_model.predict(f"[{lang_key}] {text}") pred_b, conf_b = bertic_result["label"].lower(), round(bertic_result["confidence"] * 100, 2) else: pred_b, conf_b = None, None typosquat = check_typosquatting(text) votes = [pred_a] + ([pred_b] if pred_b is not None else []) models_agree = (pred_b is None) or (pred_a == pred_b) if typosquat: prediction, confidence = "phishing", 95.0 else: prediction = "phishing" if votes.count("phishing") >= 1 else "legitimate" confidences = [c for c in [conf_a, conf_b] if c is not None] confidence = round(sum(confidences) / len(confidences), 2) threat_keys = get_threat_vectors(text, lang) tv_local = get_localized_threat_vectors(lang) flag_mini = " ".join( f'{tv_local[k]["label"]}' for k in threat_keys[:2] ) if typosquat: v = tv_local["TYPOSQUATTING"] flag_mini += f'🌐 Typosquatting' if typosquat or (prediction == "phishing" and models_agree and confidence >= THRESHOLD_HIGH): verdict, verdict_label, verdict_color = "phishing", t["batch_phishing_label"], "#dc2626" elif not models_agree or (THRESHOLD_LOW <= confidence < THRESHOLD_HIGH): verdict, verdict_label, verdict_color = "uncertain", t["batch_uncertain_label"], "#d97706" uncertain += 1 elif prediction == "phishing": verdict, verdict_label, verdict_color = "phishing", t["batch_possible_label"], "#d97706" else: verdict, verdict_label, verdict_color = "legitimate", t["batch_legit_label"], "#059669" is_correct = (verdict != "uncertain") and ((verdict == "phishing" and expected == "phishing") or (verdict == "legitimate" and expected == "legitimate")) if is_correct: correct += 1 row_bg = "#f0fdf4" if is_correct else ("#fefce8" if verdict == "uncertain" else "#fef2f2") icon = "✅" if is_correct else ("⚠️" if verdict == "uncertain" else "❌") ensemble_mini = f"A:{pred_a[:1].upper()}/{conf_a}%" + (f" B:{pred_b[:1].upper()}/{conf_b}%" if pred_b else " B:—") results.append(f"""{t['batch_summary']}
| {t['batch_technique']} | {t['batch_message']} | {t['batch_expected']} | {t['batch_verdict']} | {t['batch_models']} | {t['batch_vectors']} | {t['batch_ok']} |
|---|
{t["output_label"]}
'), gr.update(value=" "), gr.update(value=quick_tests_html), gr.update(value=t["phishing_title"]), gr.update(value=t["legit_title"]), gr.update(value=t["batch_title"]), gr.update(value=t["batch_desc"]), gr.update(label=t["upload_csv"]), gr.update(value=t["run_batch"]), gr.update(value=t["batch_output"]), gr.update(value=t["footer"]), gr.update(value=t["ex_phishing_1"][:40] + "..."), gr.update(value=t["ex_phishing_2"][:40] + "..."), gr.update(value=t["ex_legit_1"][:40] + "..."), gr.update(value=t["ex_legit_2"][:40] + "..."), ) def get_example(lang, example_key): return TRANSLATIONS[lang][example_key] # ========================= # CSS (fixed alignment and styling) # ========================= css = """ footer { display: none !important; } body, .gradio-container { background: #f0f4f8 !important; font-family: 'Segoe UI', Arial, sans-serif; } h1, h2, h3, .gr-markdown h1 { color: #1e3a5f !important; text-align: center !important; margin: 0 0 6px 0 !important; } .gr-markdown h1, .gr-markdown h1:first-child { font-size: 34px !important; font-weight: 900 !important; text-align: center !important; letter-spacing: -0.5px; margin: 0 0 12px 0 !important; } .gr-markdown p { text-align: center !important; margin: 0 0 8px 0 !important; color: #475569 !important; } /* Make the main row stretch to equal height */ .main-row { display: flex !important; align-items: stretch !important; } /* Left and right panels: make them flex containers */ #left-panel, #right-panel { background: #f1f5f9 !important; border-radius: 14px !important; border: 1px solid #e2e8f0 !important; padding: 16px !important; display: flex !important; flex-direction: column !important; justify-content: flex-start !important; height: 100% !important; } /* FIX: Align top elements perfectly */ #left-panel .form, #left-panel .wrap-input { margin-top: 0 !important; } #right-panel .gr-html { margin-top: 0 !important; } /* Ensure the output area expands and has min height */ #right-panel .gr-html { flex: 1 !important; min-height: 280px !important; } .gradio-container .form textarea, .gradio-container .form .prose, .gradio-container .form .output-wrap { background: #ffffff !important; border: 1.5px solid #cbd5e1 !important; border-radius: 12px !important; color: #1e293b !important; font-size: 13px !important; padding: 12px !important; } #left-panel .checkbox-container, #left-panel .form .checkbox-container { margin: 2px 0 !important; padding: 2px 0 !important; display: block !important; background: #f1f5f9 !important; } #left-panel .checkbox-container label, #left-panel .form .checkbox-container label { background: transparent !important; margin: 0 !important; padding: 0 !important; } button.primary { background: #1e3a5f !important; color: white !important; border: none !important; border-radius: 10px !important; font-size: 16px !important; font-weight: 700 !important; padding: 12px 32px !important; min-width: 180px !important; margin: 12px auto !important; display: block !important; cursor: pointer !important; } button.primary:hover { background: #2563eb !important; transform: translateY(-2px) !important; } table td:nth-child(6), table th:nth-child(6) { font-size: 12px !important; font-weight: 500 !important; } .top-bar { display: flex; justify-content: flex-end; margin-bottom: 8px; } .sms-label { font-size: 14px; font-weight: 600; color: #1e3a5f; margin-bottom: 4px; display: block; } .gradio-container .form .row .gr-form-row { gap: 0 !important; } """ # ========================= # UI CONSTRUCTION # ========================= with gr.Blocks(css=css, title="Ne Nasedaj 🛡️") as demo: with gr.Row(elem_classes="top-bar"): with gr.Column(scale=1): pass with gr.Column(scale=0, min_width=200): lang_radio = gr.Radio(choices=[("🇬🇧 EN", "en"), ("🇷🇸 SR", "sr")], label="🌐 Language / Jezik", value="sr", interactive=True) title_md = gr.Markdown(TRANSLATIONS["sr"]["title"]) subtitle_md = gr.Markdown(TRANSLATIONS["sr"]["subtitle"]) with gr.Row(equal_height=True, elem_classes="main-row"): with gr.Column(scale=1, elem_id="left-panel"): sms_label = gr.HTML(f'{TRANSLATIONS["sr"]["output_label"]}
') output = gr.HTML(value=" ") # Quick tests quick_tests_html = f'