Initial commit
This commit is contained in:
124
README.md
Normal file
124
README.md
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
# 🛡️ AntiScam Pro
|
||||||
|
|
||||||
|
[](https://www.python.org/)
|
||||||
|
[](LICENSE)
|
||||||
|
[](https://github.com/zvspany/antiscam-pro)
|
||||||
|
[](https://github.com/zvspany/antiscam-pro/pulls)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**AntiScam Pro** is a modern, open-source security toolkit designed to detect **phishing, scam, and spam** in messages, phone numbers, and URLs.
|
||||||
|
It combines **AI-powered analysis (BERT)**, **heuristic detection**, and **VirusTotal integration** to deliver precise, transparent, and explainable results.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ Key Features
|
||||||
|
|
||||||
|
- 🤖 **AI-powered message classification** – fine-tuned BERT model detects phishing or spam text.
|
||||||
|
- 🔗 **Smart URL inspection** – resolves redirects, short links, and checks domains via VirusTotal.
|
||||||
|
- 📞 **Phone number validation** – detects known or suspicious scam numbers.
|
||||||
|
- 🌐 **REST API + Web UI** – interact easily from a browser or programmatically.
|
||||||
|
- 📊 **Session & global statistics** – track analysis metrics over time.
|
||||||
|
- 🔒 **Privacy-first design** – no data is stored or sent externally.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/zvspany/antiscam-pro.git
|
||||||
|
cd antiscam-pro
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
If you want to enable **VirusTotal integration**, create `.env` file in the project root:
|
||||||
|
```
|
||||||
|
VIRUSTOTAL_API_KEY=your_api_key_here
|
||||||
|
FLASK_SECRET_KEY=change_this_secret
|
||||||
|
```
|
||||||
|
Then run the app:
|
||||||
|
```
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
Access the dashboard at:
|
||||||
|
👉 http://127.0.0.1:5000
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💬 Usage
|
||||||
|
**You can use AntiScam Pro either from the browser UI or the API endpoints.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧠 How It Works
|
||||||
|
AntiScam Pro applies **multi-layered analysis** for high accuracy and interpretability:
|
||||||
|
**1. Input Validation** – detects whether the user provided a message, URL, or number.
|
||||||
|
**2. Heuristic Filters** – scans for known scam keywords, shorteners, or flagged domains.
|
||||||
|
**3. AI Classification** – BERT model predicts whether text is phishing, spam, or safe.
|
||||||
|
**4. External Intelligence** – optional VirusTotal API check for additional reputation data.
|
||||||
|
**5. Decision Fusion** – merges signals into a single final verdict.
|
||||||
|
This architecture ensures both **speed** and **transparency**, making it suitable for research and production use.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧰 Tech Stack
|
||||||
|
- **🐍 Python 3** + **Flask** – lightweight and fast backend
|
||||||
|
- **🤗 HuggingFace Transformers (BERT)** – machine learning for text analysis
|
||||||
|
- **🌍 Requests** – robust HTTP handling
|
||||||
|
- **🧩 VirusTotal API** – optional link intelligence
|
||||||
|
- **⚙️ Regex** + **heuristic rules** – simple but effective early filtering
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🪶 Design Philosophy
|
||||||
|
|
||||||
|
AntiScam Pro is built around four core principles:
|
||||||
|
**🕵️♂️ Transparency** – detection logic is fully open and auditable
|
||||||
|
**⚡ Simplicity** – lightweight, minimal dependencies
|
||||||
|
**🔐 Privacy** – no user tracking, no telemetry, no cloud sync
|
||||||
|
**🧩 Extensibility** – easy to add models, blacklists, or new APIs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
Contributions are welcome!
|
||||||
|
If you’d like to enhance detection logic, optimize performance, or improve UI/UX — open a pull request or issue.
|
||||||
|
Let's build a safer digital world, together 💙
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📜 License
|
||||||
|
This project is released under the **MIT License**.
|
||||||
|
```
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 zvspany
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the “Software”), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ❤️ Acknowledgements
|
||||||
|
Built by [@zvspany](https://zvspany.github.io/bio/)
|
||||||
|
Inspired by the fight against online scams, phishing, and misinformation.
|
||||||
|
Powered by the open-source community 🌍
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
🧩 *[AntiScam Pro](https://github.com/zvspany/antiscam-pro) — because cybersecurity should be transparent, intelligent, and free.*
|
||||||
BIN
api/__pycache__/endpoints.cpython-313.pyc
Normal file
BIN
api/__pycache__/endpoints.cpython-313.pyc
Normal file
Binary file not shown.
BIN
api/__pycache__/utils.cpython-313.pyc
Normal file
BIN
api/__pycache__/utils.cpython-313.pyc
Normal file
Binary file not shown.
BIN
api/__pycache__/validators.cpython-313.pyc
Normal file
BIN
api/__pycache__/validators.cpython-313.pyc
Normal file
Binary file not shown.
BIN
api/__pycache__/virustotal.cpython-313.pyc
Normal file
BIN
api/__pycache__/virustotal.cpython-313.pyc
Normal file
Binary file not shown.
235
api/endpoints.py
Normal file
235
api/endpoints.py
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
# api/endpoints.py
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
import base64
|
||||||
|
from .utils import load_keywords, load_numbers, load_domains, load_shorteners, resolve_redirect
|
||||||
|
from .validators import Validators
|
||||||
|
from transformers import pipeline
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Import scanning function from a separate virustotal.py
|
||||||
|
from .virustotal import scan_url_with_virustotal
|
||||||
|
|
||||||
|
# VirusTotal API key is not needed here since the function in virustotal.py uses it
|
||||||
|
|
||||||
|
# Initialize AI model
|
||||||
|
try:
|
||||||
|
# suppress_warnings=True silences warnings during model loading
|
||||||
|
spam_classifier = pipeline("text-classification", model="mrm8488/bert-tiny-finetuned-sms-spam-detection")
|
||||||
|
logger.info("AI spam detection model loaded successfully.")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to load AI model: {e}")
|
||||||
|
spam_classifier = None
|
||||||
|
|
||||||
|
# check_message - LOGIC FUNCTION
|
||||||
|
def check_message(text: str) -> dict:
|
||||||
|
"""Checks a text message for spam and suspicious words."""
|
||||||
|
result = {
|
||||||
|
"suspicious_words": [],
|
||||||
|
"ai_result": None,
|
||||||
|
"is_suspicious": False
|
||||||
|
}
|
||||||
|
|
||||||
|
keywords = load_keywords() # Assume load_keywords() works correctly
|
||||||
|
keywords = [kw.strip() for kw in keywords if kw.strip()] # Filter empty keywords
|
||||||
|
|
||||||
|
# Check for suspicious keywords (case-insensitive)
|
||||||
|
found_keywords = [kw for kw in keywords if kw.lower() in text.lower()]
|
||||||
|
result["suspicious_words"] = found_keywords
|
||||||
|
|
||||||
|
if spam_classifier:
|
||||||
|
try:
|
||||||
|
# Trim message to first 512 tokens for BERT
|
||||||
|
max_len = 512
|
||||||
|
if len(text.split()) > max_len:
|
||||||
|
text_for_ai = " ".join(text.split()[:max_len])
|
||||||
|
logger.warning(f"Message trimmed to {max_len} words for AI analysis.")
|
||||||
|
else:
|
||||||
|
text_for_ai = text
|
||||||
|
|
||||||
|
prediction = spam_classifier(text_for_ai)[0]
|
||||||
|
label = prediction["label"]
|
||||||
|
confidence = round(prediction["score"], 4)
|
||||||
|
result["ai_result"] = {
|
||||||
|
"label": "SPAM" if label == "LABEL_1" else "HAM", # LABEL_1 assumed to be SPAM
|
||||||
|
"confidence": confidence
|
||||||
|
}
|
||||||
|
|
||||||
|
# Suspicion if AI >= 0.6 OR keywords found
|
||||||
|
result["is_suspicious"] = (label == "LABEL_1" and confidence >= 0.6) or bool(result["suspicious_words"])
|
||||||
|
except Exception as ai_e:
|
||||||
|
logger.error(f"Error during AI message classification: {ai_e}")
|
||||||
|
result["ai_result"] = {"label": "ERROR", "confidence": 0}
|
||||||
|
# If AI fails, use only keyword check
|
||||||
|
result["is_suspicious"] = bool(result["suspicious_words"])
|
||||||
|
else:
|
||||||
|
result["is_suspicious"] = bool(result["suspicious_words"])
|
||||||
|
|
||||||
|
logger.info(f"check_message result: {result}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
# check_phone - LOGIC FUNCTION
|
||||||
|
def check_phone(number: str) -> dict:
|
||||||
|
"""Checks a phone number for valid format and known scams."""
|
||||||
|
known_scams = load_numbers()
|
||||||
|
known_scams = {num.strip() for num in known_scams if num.strip()}
|
||||||
|
|
||||||
|
is_valid = Validators.is_valid_phone(number)
|
||||||
|
number_for_check = number.lstrip('+') # Remove '+' for database check
|
||||||
|
is_suspicious = number_for_check in known_scams
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"is_valid": is_valid,
|
||||||
|
"is_suspicious": is_suspicious
|
||||||
|
}
|
||||||
|
logger.info(f"check_phone result for {number}: {result}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
# check_link - LOGIC FUNCTION
|
||||||
|
def check_link(link: str) -> dict:
|
||||||
|
"""Checks a URL for suspicious patterns, known scam domains, shorteners, and scans with VirusTotal."""
|
||||||
|
logger.info(f"Checking link: {link}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
link_resolved = resolve_redirect(link)
|
||||||
|
logger.info(f"Resolved link: {link_resolved}")
|
||||||
|
except Exception as resolve_e:
|
||||||
|
logger.error(f"Error resolving link {link}: {resolve_e}")
|
||||||
|
return {
|
||||||
|
"is_valid": False,
|
||||||
|
"is_suspicious": True,
|
||||||
|
"details": [{
|
||||||
|
"text": f"Error resolving redirect: {resolve_e}",
|
||||||
|
"data-pl": f"Błąd rozwiązywania przekierowania: {resolve_e}",
|
||||||
|
"data-en": f"Error resolving redirect: {resolve_e}"
|
||||||
|
}],
|
||||||
|
"source": "local"
|
||||||
|
}
|
||||||
|
|
||||||
|
link_clean = link_resolved.lower()
|
||||||
|
suspicious_reasons = []
|
||||||
|
|
||||||
|
is_valid = Validators.is_valid_url(link_resolved)
|
||||||
|
if not is_valid:
|
||||||
|
logger.warning(f"Resolved link is not a valid URL: {link_resolved}")
|
||||||
|
return {
|
||||||
|
"is_valid": False,
|
||||||
|
"is_suspicious": False,
|
||||||
|
"details": [{
|
||||||
|
"text": "Invalid resolved URL format",
|
||||||
|
"data-pl": "Niepoprawny format rozwiązanego URL",
|
||||||
|
"data-en": "Invalid resolved URL format"
|
||||||
|
}],
|
||||||
|
"source": "local"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Local analysis - suspicious patterns in URL
|
||||||
|
suspicious_patterns = ["free", "gift", "login", "verify", "paypal", "paypa1", "bank", ".ru", ".cn"]
|
||||||
|
for pattern in suspicious_patterns:
|
||||||
|
if pattern in link_clean:
|
||||||
|
suspicious_reasons.append({
|
||||||
|
"text": f"Suspicious pattern found in URL: {pattern}",
|
||||||
|
"data-pl": f"Podejrzany wzorzec '{pattern}' znaleziony w adresie URL.",
|
||||||
|
"data-en": f"Suspicious pattern '{pattern}' found in the URL."
|
||||||
|
})
|
||||||
|
logger.info(f"Suspicious pattern '{pattern}' found in link: {link_resolved}")
|
||||||
|
|
||||||
|
# Local analysis - scam domains from database
|
||||||
|
scam_domains = load_domains()
|
||||||
|
scam_domains = [domain.strip() for domain in scam_domains if domain.strip()]
|
||||||
|
for domain in scam_domains:
|
||||||
|
if domain and domain in link_clean:
|
||||||
|
suspicious_reasons.append({
|
||||||
|
"text": f"Domain '{domain}' marked as suspicious in our database.",
|
||||||
|
"data-pl": f"Domena '{domain}' oznaczona jako podejrzana w naszej bazie danych.",
|
||||||
|
"data-en": f"Domain '{domain}' marked as suspicious in our database."
|
||||||
|
})
|
||||||
|
logger.info(f"Suspicious domain '{domain}' found in link: {link_resolved}")
|
||||||
|
|
||||||
|
# Local analysis - URL shorteners (check original link)
|
||||||
|
shorteners = load_shorteners()
|
||||||
|
shorteners = [short.strip() for short in shorteners if short.strip()]
|
||||||
|
is_shortened = False
|
||||||
|
for short in shorteners:
|
||||||
|
if short and short in link.lower():
|
||||||
|
is_shortened = True
|
||||||
|
suspicious_reasons.append({
|
||||||
|
"text": f"Link shortener detected in original link: {short}",
|
||||||
|
"data-pl": f"Wykryto usługę skracania URL w oryginalnym linku: {short}",
|
||||||
|
"data-en": f"Link shortener detected in original link: {short}"
|
||||||
|
})
|
||||||
|
logger.info(f"URL shortener '{short}' detected in original link: {link}")
|
||||||
|
|
||||||
|
local_is_suspicious = bool(suspicious_reasons)
|
||||||
|
|
||||||
|
# VirusTotal scan: only if local suspicious findings or short link
|
||||||
|
vt_result = {"detected": False, "positives": 0, "total": 0, "scan_date": "N/A", "error": "Not scanned locally"}
|
||||||
|
if local_is_suspicious or is_shortened:
|
||||||
|
logger.info(f"Local suspicious or short link -> checking VirusTotal for: {link_resolved}")
|
||||||
|
vt_result = scan_url_with_virustotal(link_resolved)
|
||||||
|
logger.info(f"VirusTotal result for {link_resolved}: {vt_result}")
|
||||||
|
|
||||||
|
vt_detected = vt_result.get("detected", False)
|
||||||
|
vt_positives = vt_result.get("positives", 0)
|
||||||
|
vt_total = vt_result.get("total", 0)
|
||||||
|
vt_scan_date = vt_result.get("scan_date", "N/A")
|
||||||
|
vt_error = vt_result.get("error")
|
||||||
|
|
||||||
|
combined_details = []
|
||||||
|
|
||||||
|
if local_is_suspicious:
|
||||||
|
combined_details.extend(suspicious_reasons)
|
||||||
|
|
||||||
|
if vt_result.get('error') != "Not scanned locally":
|
||||||
|
if vt_error and vt_error != "Not scanned locally":
|
||||||
|
vt_summary_text = f"VirusTotal: Error - {vt_error}"
|
||||||
|
vt_summary_pl = f"VirusTotal: Błąd - {vt_error}"
|
||||||
|
vt_summary_en = f"VirusTotal: Error - {vt_error}"
|
||||||
|
else:
|
||||||
|
vt_summary_text = f"VirusTotal: {vt_positives} / {vt_total} engines flagged the link."
|
||||||
|
vt_summary_pl = f"VirusTotal: {vt_positives} / {vt_total} silników oznaczyło link."
|
||||||
|
vt_summary_en = f"VirusTotal: {vt_positives} / {vt_total} engines flagged the link."
|
||||||
|
|
||||||
|
if vt_scan_date and vt_scan_date != "N/A":
|
||||||
|
vt_summary_text += f" Scan date: {vt_scan_date}."
|
||||||
|
vt_summary_pl += f" Scan date: {vt_scan_date}."
|
||||||
|
vt_summary_en += f" Scan date: {vt_scan_date}."
|
||||||
|
|
||||||
|
combined_details.append({
|
||||||
|
"text": vt_summary_text,
|
||||||
|
"data-pl": vt_summary_pl,
|
||||||
|
"data-en": vt_summary_en
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
logger.info(f"VirusTotal scan skipped for link: {link_resolved} (local analysis clean and not shortened).")
|
||||||
|
|
||||||
|
final_is_suspicious = local_is_suspicious or vt_detected
|
||||||
|
|
||||||
|
if not combined_details:
|
||||||
|
combined_details.append({
|
||||||
|
"text": "No suspicious activity detected based on available checks.",
|
||||||
|
"data-pl": "Nie wykryto podejrzanej aktywności na podstawie dostępnych sprawdzeń.",
|
||||||
|
"data-en": "No suspicious activity detected based on available checks."
|
||||||
|
})
|
||||||
|
source = "none"
|
||||||
|
elif local_is_suspicious and vt_detected:
|
||||||
|
source = "combined"
|
||||||
|
elif vt_detected:
|
||||||
|
source = "virustotal"
|
||||||
|
elif local_is_suspicious:
|
||||||
|
source = "local"
|
||||||
|
else:
|
||||||
|
source = "unknown"
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"is_valid": is_valid,
|
||||||
|
"is_suspicious": final_is_suspicious,
|
||||||
|
"details": combined_details,
|
||||||
|
"source": source
|
||||||
|
}
|
||||||
|
logger.info(f"Final result for check_link ({link}): {result}")
|
||||||
|
return result
|
||||||
|
|
||||||
53
api/utils.py
Normal file
53
api/utils.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# api/utils.py
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
def load_keywords():
|
||||||
|
"""Load scam-related keywords from file."""
|
||||||
|
try:
|
||||||
|
with open("data/scam_keywords.txt", "r", encoding="utf-8") as f:
|
||||||
|
# Ensure keywords are converted to lowercase
|
||||||
|
return [line.strip().lower() for line in f]
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("File 'scam_keywords.txt' not found.")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def load_numbers():
|
||||||
|
"""Load scam phone numbers from file."""
|
||||||
|
try:
|
||||||
|
with open("data/scam_numbers.txt", "r", encoding="utf-8") as f:
|
||||||
|
return [line.strip() for line in f]
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("File 'scam_numbers.txt' not found.")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def load_domains():
|
||||||
|
"""Load known scam domains from file."""
|
||||||
|
try:
|
||||||
|
with open("data/scam_domains.txt", "r", encoding="utf-8") as f:
|
||||||
|
return [line.strip().lower() for line in f]
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("File 'scam_domains.txt' not found.")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def load_shorteners():
|
||||||
|
"""Load known URL shorteners from file."""
|
||||||
|
try:
|
||||||
|
with open("data/url_shorteners.txt", "r", encoding="utf-8") as f:
|
||||||
|
return [line.strip().lower() for line in f]
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("File 'url_shorteners.txt' not found.")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def resolve_redirect(url: str) -> str:
|
||||||
|
"""
|
||||||
|
Resolves redirects for shortened URLs like bit.ly, tinyurl, etc.
|
||||||
|
Returns the final URL or the original if redirect fails.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = requests.head(url, allow_redirects=True, timeout=5)
|
||||||
|
return response.url
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[WARN] Failed to resolve redirect: {e}")
|
||||||
|
return url
|
||||||
|
|
||||||
102
api/validators.py
Normal file
102
api/validators.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import re
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
class Validators:
|
||||||
|
@staticmethod
|
||||||
|
def is_valid_phone(number: str) -> bool:
|
||||||
|
"""
|
||||||
|
Validate phone number format (E.164 with optional +)
|
||||||
|
Allowed formats:
|
||||||
|
+48123456789
|
||||||
|
48123456789
|
||||||
|
123456789
|
||||||
|
"""
|
||||||
|
return re.match(r"^\+?\d[\d\s-]{8,14}\d$", number) is not None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_valid_email(email: str) -> bool:
|
||||||
|
"""
|
||||||
|
Validate email format
|
||||||
|
"""
|
||||||
|
return re.match(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", email) is not None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_valid_password(password: str, min_length: int = 8) -> bool:
|
||||||
|
"""
|
||||||
|
Validate password:
|
||||||
|
- Minimum length
|
||||||
|
- At least one digit
|
||||||
|
- At least one uppercase
|
||||||
|
- At least one lowercase
|
||||||
|
"""
|
||||||
|
if len(password) < min_length:
|
||||||
|
return False
|
||||||
|
if not re.search(r"\d", password):
|
||||||
|
return False
|
||||||
|
if not re.search(r"[A-Z]", password):
|
||||||
|
return False
|
||||||
|
if not re.search(r"[a-z]", password):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_valid_username(username: str, min_length: int = 3, max_length: int = 20) -> bool:
|
||||||
|
"""
|
||||||
|
Validate username:
|
||||||
|
- Only alphanumeric and underscores
|
||||||
|
- Length between min and max
|
||||||
|
"""
|
||||||
|
return (re.match(r"^[a-zA-Z0-9_]+$", username) is not None and
|
||||||
|
min_length <= len(username) <= max_length)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_valid_postal_code(code: str, country: str = 'PL') -> bool:
|
||||||
|
"""
|
||||||
|
Validate postal code format for different countries
|
||||||
|
Default: Polish format (00-000)
|
||||||
|
"""
|
||||||
|
if country == 'PL':
|
||||||
|
return re.match(r"^\d{2}-\d{3}$", code) is not None
|
||||||
|
# Add other country formats as needed
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_valid_url(url: str) -> bool:
|
||||||
|
"""
|
||||||
|
Validate URL format
|
||||||
|
"""
|
||||||
|
return re.match(
|
||||||
|
r"^(https?://)?(www\.)?[a-z0-9-]+(\.[a-z]{2,}){1,}(/.*)?$",
|
||||||
|
url,
|
||||||
|
re.IGNORECASE
|
||||||
|
) is not None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_length_valid(text: str, min_len: int = 0, max_len: Optional[int] = None) -> bool:
|
||||||
|
"""
|
||||||
|
Validate text length
|
||||||
|
"""
|
||||||
|
if max_len is None:
|
||||||
|
return len(text) >= min_len
|
||||||
|
return min_len <= len(text) <= max_len
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_numeric(text: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check if text contains only digits
|
||||||
|
"""
|
||||||
|
return text.isdigit()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_alpha(text: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check if text contains only letters
|
||||||
|
"""
|
||||||
|
return text.isalpha()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_alphanumeric(text: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check if text contains only letters and digits
|
||||||
|
"""
|
||||||
|
return text.isalnum()
|
||||||
115
api/virustotal.py
Normal file
115
api/virustotal.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
# api/virustotal.py
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
import base64
|
||||||
|
import logging
|
||||||
|
from datetime import datetime # Only import datetime
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Retrieve API key from ENV or use default (should be changed!)
|
||||||
|
VIRUSTOTAL_API_KEY = os.getenv("VT_API_KEY")
|
||||||
|
|
||||||
|
# Warning if the key is not set
|
||||||
|
if not VIRUSTOTAL_API_KEY or VIRUSTOTAL_API_KEY == "YOUR_API_KEY":
|
||||||
|
logger.warning("VIRUSTOTAL_API_KEY is not set in environment variables or default value was not changed.")
|
||||||
|
# Optionally, you might want to raise an error or disable VT checks entirely if the key is missing.
|
||||||
|
|
||||||
|
# Removed imports of pytz and tzlocal and SERVER_LOCAL_TZ detection logic
|
||||||
|
|
||||||
|
def scan_url_with_virustotal(url: str) -> dict:
|
||||||
|
"""
|
||||||
|
Scans a URL using VirusTotal API v3.
|
||||||
|
Retrieves an existing scan report for the given URL.
|
||||||
|
(Does not initiate a new scan if the report does not exist)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url (str): URL to scan.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary containing VirusTotal scan results.
|
||||||
|
{
|
||||||
|
"detected": bool, # True if malicious or suspicious > 0
|
||||||
|
"positives": int, # Sum of malicious and suspicious counts
|
||||||
|
"total": int, # Sum of counts for harmless, malicious, suspicious, undetected, timeout, failure
|
||||||
|
"scan_date": str, # Formatted scan date string in UTC or "N/A"
|
||||||
|
"error": str # Error message if request fails
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
if not VIRUSTOTAL_API_KEY or VIRUSTOTAL_API_KEY == "YOUR_API_KEY":
|
||||||
|
return {"detected": False, "positives": 0, "total": 0, "scan_date": "N/A", "error": "API key missing or default"}
|
||||||
|
|
||||||
|
api_url_base = "https://www.virustotal.com/api/v3/urls/"
|
||||||
|
headers = {"x-apikey": VIRUSTOTAL_API_KEY}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# VirusTotal API v3 requires base64url encoded URL without padding
|
||||||
|
# https://developers.virustotal.com/v3.0/reference/#urls-id
|
||||||
|
encoded_url = base64.urlsafe_b64encode(url.encode()).decode().strip("=")
|
||||||
|
|
||||||
|
# URL to fetch analysis
|
||||||
|
analysis_url = f"{api_url_base}{encoded_url}"
|
||||||
|
|
||||||
|
logger.info(f"Querying VirusTotal for URL: {url}")
|
||||||
|
response = requests.get(analysis_url, headers=headers)
|
||||||
|
|
||||||
|
# Check response status
|
||||||
|
if response.status_code == 404:
|
||||||
|
logger.info(f"VirusTotal: URL not found in database: {url}")
|
||||||
|
return {"detected": False, "positives": 0, "total": 0, "scan_date": "N/A", "message": "URL not found in VT database"}
|
||||||
|
elif response.status_code == 401:
|
||||||
|
logger.error("VirusTotal API error: Invalid API key")
|
||||||
|
return {"detected": False, "positives": 0, "total": 0, "scan_date": "N/A", "error": "Invalid API key"}
|
||||||
|
elif response.status_code == 429:
|
||||||
|
logger.warning("VirusTotal API error: Rate limit exceeded")
|
||||||
|
return {"detected": False, "positives": 0, "total": 0, "scan_date": "N/A", "error": "Rate limit exceeded"}
|
||||||
|
elif response.status_code >= 400: # Handle other 4xx/5xx HTTP errors
|
||||||
|
error_message = f"HTTP error {response.status_code}"
|
||||||
|
try:
|
||||||
|
error_data = response.json()
|
||||||
|
error_message += f": {error_data.get('error', {}).get('message', 'Unknown VT error')}"
|
||||||
|
except:
|
||||||
|
pass # Ignore if JSON parsing fails
|
||||||
|
logger.error(f"VirusTotal HTTP error for {url}: {error_message}")
|
||||||
|
return {"detected": False, "positives": 0, "total": 0, "scan_date": "N/A", "error": error_message}
|
||||||
|
|
||||||
|
# If status 200 OK, process data
|
||||||
|
data = response.json()
|
||||||
|
attributes = data.get("data", {}).get("attributes", {})
|
||||||
|
stats = attributes.get("last_analysis_stats", {})
|
||||||
|
|
||||||
|
malicious = stats.get("malicious", 0)
|
||||||
|
suspicious = stats.get("suspicious", 0)
|
||||||
|
|
||||||
|
# Calculate total engines based on the stats provided
|
||||||
|
total_engines = stats.get("harmless", 0) + malicious + suspicious + stats.get("undetected", 0) + stats.get("timeout", 0) + stats.get("failure", 0)
|
||||||
|
|
||||||
|
scan_date_ts = attributes.get("last_analysis_date") # Timestamp (seconds since epoch UTC)
|
||||||
|
scan_date_str = "N/A"
|
||||||
|
if scan_date_ts:
|
||||||
|
try:
|
||||||
|
# Convert timestamp to naive UTC datetime object
|
||||||
|
utc_dt = datetime.utcfromtimestamp(scan_date_ts)
|
||||||
|
# Format date and time, append " UTC"
|
||||||
|
scan_date_str = utc_dt.strftime('%Y-%m-%d %H:%M:%S') + ' UTC'
|
||||||
|
|
||||||
|
except Exception as date_e:
|
||||||
|
logger.error(f"Error formatting VirusTotal scan date timestamp {scan_date_ts}: {date_e}")
|
||||||
|
scan_date_str = "Invalid Date Format"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"detected": (malicious + suspicious) > 0,
|
||||||
|
"positives": malicious + suspicious,
|
||||||
|
"total": total_engines,
|
||||||
|
"scan_date": scan_date_str, # Return formatted date string in UTC
|
||||||
|
"error": None # No error
|
||||||
|
}
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as req_err:
|
||||||
|
logger.error(f"VirusTotal request failed for {url}: {req_err}")
|
||||||
|
return {"detected": False, "positives": 0, "total": 0, "scan_date": "N/A", "error": f"Request failed: {req_err}"}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"An unexpected error occurred during VirusTotal check for {url}: {e}")
|
||||||
|
return {"detected": False, "positives": 0, "total": 0, "scan_date": "N/A", "error": f"Unexpected error: {e}"}
|
||||||
|
|
||||||
189
app.py
Normal file
189
app.py
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
# app.py
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from flask import Flask, render_template, request, jsonify, session, redirect, url_for
|
||||||
|
|
||||||
|
# Import functions from api/endpoints.py
|
||||||
|
# check_link is assumed to handle VT internally and return combined result for 'link' key
|
||||||
|
from api.endpoints import check_message, check_phone, check_link
|
||||||
|
# Import Validators from api/validators.py - Needed for type detection
|
||||||
|
from api.validators import Validators
|
||||||
|
|
||||||
|
# Konfiguracja logowania
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
# Setting secret_key is crucial for session security. Change this in production!
|
||||||
|
app.secret_key = os.getenv("FLASK_SECRET_KEY", "default_key_that_should_be_changed")
|
||||||
|
|
||||||
|
# Global statistics (since server start)
|
||||||
|
GLOBAL_STATS = {
|
||||||
|
"messages_checked": 0,
|
||||||
|
"phones_checked": 0,
|
||||||
|
"links_checked": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
def increment_stat(key):
|
||||||
|
"""Increments session and global stats."""
|
||||||
|
# Session
|
||||||
|
session[key] = session.get(key, 0) + 1
|
||||||
|
# Global
|
||||||
|
GLOBAL_STATS[key] += 1
|
||||||
|
|
||||||
|
# ZASTĄP CAŁĄ FUNKCJĘ index PONIŻSZYM KODEM
|
||||||
|
@app.route("/", methods=["GET", "POST"])
|
||||||
|
def index():
|
||||||
|
"""
|
||||||
|
Handles the main page, processing form submissions (POST)
|
||||||
|
and displaying results after redirect (GET).
|
||||||
|
Implements Post/Redirect/Get (PRG) pattern.
|
||||||
|
Processes a single input field, detecting data type (Link, Phone, Message).
|
||||||
|
"""
|
||||||
|
user_ip = request.remote_addr
|
||||||
|
|
||||||
|
# Initialize session stats if they don't exist
|
||||||
|
if "messages_checked" not in session:
|
||||||
|
session["messages_checked"] = 0
|
||||||
|
session["phones_checked"] = 0
|
||||||
|
session["links_checked"] = 0
|
||||||
|
|
||||||
|
logger.info(f"Request to index ({request.method}) from IP: %s", user_ip)
|
||||||
|
|
||||||
|
# --- Handle POST Request (Process & Store, then Redirect) ---
|
||||||
|
if request.method == "POST":
|
||||||
|
# Clear previous results from session before processing new ones
|
||||||
|
session.pop('check_results', None)
|
||||||
|
|
||||||
|
# Get data from the single input field named "input_data"
|
||||||
|
input_data = request.form.get("input_data", "").strip() # Get and strip whitespace
|
||||||
|
|
||||||
|
current_check_results = {}
|
||||||
|
|
||||||
|
if input_data:
|
||||||
|
logger.info(f"Received input data: '{input_data}'")
|
||||||
|
|
||||||
|
# --- Type Detection Logic ---
|
||||||
|
# Implement a simple prioritization: Link -> Phone -> Message
|
||||||
|
# This order can be adjusted based on expected input types
|
||||||
|
|
||||||
|
# 1. Check if it's a Link (URL) first
|
||||||
|
# Use Validators.is_valid_url from api/validators.py
|
||||||
|
if Validators.is_valid_url(input_data):
|
||||||
|
logger.info(f"Input detected as URL: {input_data}")
|
||||||
|
# Call check_link. Based on your endpoints.py, it returns the combined result dictionary for 'link'
|
||||||
|
link_result = check_link(input_data)
|
||||||
|
# Store the result dictionary under the 'link' key for the HTML template to read
|
||||||
|
current_check_results["link"] = link_result
|
||||||
|
increment_stat("links_checked") # Increment link stat
|
||||||
|
logger.info(f"Processed link check. Result: {link_result.get('is_suspicious', 'N/A')}")
|
||||||
|
|
||||||
|
# 2. Else, check if it's a Phone Number
|
||||||
|
# Use Validators.is_valid_phone from api/validators.py
|
||||||
|
elif Validators.is_valid_phone(input_data):
|
||||||
|
logger.info(f"Input detected as Phone: {input_data}")
|
||||||
|
phone_result = check_phone(input_data)
|
||||||
|
# Store the result dictionary under the 'phone' key for the HTML template
|
||||||
|
current_check_results["phone"] = phone_result
|
||||||
|
increment_stat("phones_checked") # Increment phone stat
|
||||||
|
logger.info(f"Processed phone check. Result: {phone_result.get('is_suspicious', 'N/A')}")
|
||||||
|
|
||||||
|
# 3. Else, if it's neither a valid URL nor a valid phone number, assume it's a Message
|
||||||
|
else:
|
||||||
|
logger.info(f"Input treated as Message: {input_data}")
|
||||||
|
message_result = check_message(input_data)
|
||||||
|
# Store the result dictionary under the 'message' key for the HTML template
|
||||||
|
current_check_results["message"] = message_result
|
||||||
|
increment_stat("messages_checked") # Increment message stat
|
||||||
|
logger.info(f"Processed message check. Result: {message_result.get('is_suspicious', 'N/A')}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.info("Received empty input data.")
|
||||||
|
# If input is empty, current_check_results will be empty.
|
||||||
|
# The HTML template is designed to handle this gracefully (no results displayed).
|
||||||
|
|
||||||
|
# Store the results from this single check in the session to be retrieved by the subsequent GET request
|
||||||
|
session['check_results'] = current_check_results
|
||||||
|
|
||||||
|
# Redirect to the same URL with GET method to display results and prevent form re-submission on refresh
|
||||||
|
logger.info("Redirecting after POST.")
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
# --- Handle GET Request (Retrieve & Display) ---
|
||||||
|
else: # request.method == "GET"
|
||||||
|
# Retrieve the results from session if they exist (after a POST redirect)
|
||||||
|
# Use session.pop to get the value and remove it in one step. Defaults to empty dict if no results in session.
|
||||||
|
result = session.pop('check_results', {})
|
||||||
|
logger.info(f"Handling GET request. Results retrieved from session: {bool(result)}")
|
||||||
|
|
||||||
|
# Prepare session stats to pass to the template
|
||||||
|
session_stats = {
|
||||||
|
"messages_checked": session.get("messages_checked", 0),
|
||||||
|
"phones_checked": session.get("phones_checked", 0),
|
||||||
|
"links_checked": session.get("links_checked", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Render the template with the retrieved results (or empty result dict for a normal initial GET)
|
||||||
|
# The 'result' dictionary will contain 'message', 'phone', or 'link' key based on input type detected in POST
|
||||||
|
return render_template("index.html",
|
||||||
|
result=result, # Pass the result dictionary (can be empty or contain one check result)
|
||||||
|
global_stats=GLOBAL_STATS,
|
||||||
|
session_stats=session_stats)
|
||||||
|
|
||||||
|
# --- API ENDPOINTS ---
|
||||||
|
# These endpoints are designed for specific input types and typically return JSON.
|
||||||
|
# They remain unchanged as they are separate from the main single-input form.
|
||||||
|
|
||||||
|
@app.route("/api/check_message", methods=["POST"])
|
||||||
|
def api_check_message():
|
||||||
|
"""API endpoint for message check."""
|
||||||
|
data = request.get_json()
|
||||||
|
message = data.get("message", "")
|
||||||
|
if not message:
|
||||||
|
logger.warning("API check_message: Missing 'message' parameter.")
|
||||||
|
return jsonify({"error": "Missing 'message' parameter."}), 400
|
||||||
|
|
||||||
|
message_result = check_message(message)
|
||||||
|
increment_stat("messages_checked") # Increment message stat for API
|
||||||
|
logger.info(f"API check_message: Result: {message_result.get('is_suspicious', 'N/A')}")
|
||||||
|
return jsonify(message_result)
|
||||||
|
|
||||||
|
@app.route("/api/check_phone", methods=["POST"])
|
||||||
|
def api_check_phone():
|
||||||
|
"""API endpoint for phone check."""
|
||||||
|
data = request.get_json()
|
||||||
|
phone = data.get("phone_number", "")
|
||||||
|
if not phone:
|
||||||
|
logger.warning("API check_phone: Missing 'phone_number' parameter.")
|
||||||
|
return jsonify({"error": "Missing 'phone_number' parameter."}), 400
|
||||||
|
|
||||||
|
phone_result = check_phone(phone)
|
||||||
|
increment_stat("phones_checked") # Increment phone stat for API
|
||||||
|
logger.info(f"API check_phone: Result: {phone_result.get('is_suspicious', 'N/A')}")
|
||||||
|
return jsonify(phone_result)
|
||||||
|
|
||||||
|
@app.route("/api/check_link", methods=["POST"])
|
||||||
|
def api_check_link():
|
||||||
|
"""API endpoint for link check."""
|
||||||
|
data = request.get_json()
|
||||||
|
url = data.get("url", "")
|
||||||
|
if not url:
|
||||||
|
logger.warning("API check_link: Missing 'url' parameter.")
|
||||||
|
return jsonify({"error": "Missing 'url' parameter."}), 400
|
||||||
|
|
||||||
|
# Note: check_link in your endpoints.py handles VT internally and returns the combined dict
|
||||||
|
link_result = check_link(url)
|
||||||
|
|
||||||
|
increment_stat("links_checked") # Increment link stat for API
|
||||||
|
logger.info(f"API check_link: Result: {link_result.get('is_suspicious', 'N/A')}, Source: {link_result.get('source', 'N/A')}")
|
||||||
|
# Return the combined result dictionary from check_link
|
||||||
|
return jsonify(link_result)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Application Entry Point ---
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Port from environment variable or default 5000
|
||||||
|
port = int(os.environ.get("PORT", 5000))
|
||||||
|
# host='0.0.0.0' allows external access (e.g., in Docker)
|
||||||
|
# debug=True is useful during development
|
||||||
|
app.run(host="0.0.0.0", port=port, debug=True)
|
||||||
183590
data/scam_domains.txt
Normal file
183590
data/scam_domains.txt
Normal file
File diff suppressed because it is too large
Load Diff
347
data/scam_keywords.txt
Normal file
347
data/scam_keywords.txt
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
darmowe pieniądze
|
||||||
|
zyskaj natychmiast
|
||||||
|
pilna oferta
|
||||||
|
oferta tylko teraz
|
||||||
|
ograniczona oferta
|
||||||
|
ostateczna oferta
|
||||||
|
zyskaj teraz
|
||||||
|
potwierdź dane
|
||||||
|
weryfikacja konta
|
||||||
|
nagroda czeka
|
||||||
|
potrzebujemy twoich danych
|
||||||
|
potwierdzenie tożsamości
|
||||||
|
potwierdź numer telefonu
|
||||||
|
sprawdź konto
|
||||||
|
bezzwłoczna aktualizacja
|
||||||
|
zaktualizuj dane
|
||||||
|
wygasłe konto
|
||||||
|
decyzja musi być podjęta teraz
|
||||||
|
twoje konto zostało zablokowane
|
||||||
|
pilne konto
|
||||||
|
zaloguj się teraz
|
||||||
|
konkurs wygrany
|
||||||
|
nagroda czeka na ciebie
|
||||||
|
darmowy prezent
|
||||||
|
zyskaj miliony
|
||||||
|
potwierdź adres e-mail
|
||||||
|
wejdź na naszą stronę
|
||||||
|
przyjdź po swoją nagrodę
|
||||||
|
bezwysiłkowe zarobki
|
||||||
|
proste rozwiązanie
|
||||||
|
płatności bez ryzyka
|
||||||
|
inwestycje gwarantowane
|
||||||
|
weź udział teraz
|
||||||
|
nagroda dla ciebie
|
||||||
|
bezpłatna rejestracja
|
||||||
|
specjalna oferta
|
||||||
|
darmowe testy
|
||||||
|
otrzymaj kod rabatowy
|
||||||
|
zarejestruj się teraz
|
||||||
|
szybkie pieniądze
|
||||||
|
wygraj teraz
|
||||||
|
zapłać teraz
|
||||||
|
oferta tylko dla ciebie
|
||||||
|
kliknij tutaj
|
||||||
|
wejdź teraz
|
||||||
|
ograniczony dostęp
|
||||||
|
bądź pierwszy
|
||||||
|
podaj swój numer
|
||||||
|
potwierdź swoje dane
|
||||||
|
zabezpieczenia konta
|
||||||
|
natychmiastowy dostęp
|
||||||
|
musisz kliknąć
|
||||||
|
dzięki za zaufanie
|
||||||
|
otwórz teraz
|
||||||
|
kluczowe informacje
|
||||||
|
twoje konto zostało zawieszone
|
||||||
|
konto zablokowane
|
||||||
|
zaloguj się, aby odblokować
|
||||||
|
pożyczka bez weryfikacji
|
||||||
|
pożyczka online
|
||||||
|
zainwestuj teraz
|
||||||
|
szybka pożyczka
|
||||||
|
weź kredyt teraz
|
||||||
|
kryptowaluty
|
||||||
|
bitcoin inwestycje
|
||||||
|
wypłać teraz
|
||||||
|
twój bonus
|
||||||
|
zyskowna inwestycja
|
||||||
|
prosta metoda
|
||||||
|
zarabiaj online
|
||||||
|
zyskaj dodatkowy dochód
|
||||||
|
nie czekaj
|
||||||
|
twoje dane są wymagane
|
||||||
|
weryfikacja danych
|
||||||
|
odblokuj dostęp
|
||||||
|
oferta wyłącznie dla ciebie
|
||||||
|
bezzwłoczna weryfikacja
|
||||||
|
automatyczne zyski
|
||||||
|
zysk w 24 godziny
|
||||||
|
wejdź na naszą stronę
|
||||||
|
płatność online
|
||||||
|
kliknij, aby kontynuować
|
||||||
|
sprawdzony sposób na zarobek
|
||||||
|
płatności gwarantowane
|
||||||
|
zyskaj dostęp teraz
|
||||||
|
kliknij, aby wygrać
|
||||||
|
przekaż pieniądze teraz
|
||||||
|
trendy inwestycyjne
|
||||||
|
darmowa próbka
|
||||||
|
wygraj nagrodę
|
||||||
|
kliknij, aby rozpocząć
|
||||||
|
darmowa aplikacja
|
||||||
|
zarejestruj się za darmo
|
||||||
|
wykorzystaj teraz
|
||||||
|
oferta bez zobowiązań
|
||||||
|
pełna gwarancja
|
||||||
|
natychmiastowy dostęp do konta
|
||||||
|
szansa na wygraną
|
||||||
|
wyślij teraz
|
||||||
|
czekamy na Ciebie
|
||||||
|
potwierdź teraz
|
||||||
|
tylko dla wybranych
|
||||||
|
bądź jednym z wygranych
|
||||||
|
promocja ograniczona czasowo
|
||||||
|
wejdź i sprawdź
|
||||||
|
usunięcie konta
|
||||||
|
wygodna pożyczka
|
||||||
|
sprawdź saldo
|
||||||
|
oferta bez ryzyka
|
||||||
|
wypełnij formularz
|
||||||
|
twoje dane są weryfikowane
|
||||||
|
zarejestruj się na platformie
|
||||||
|
sprawdź wyniki
|
||||||
|
oferta tylko dzisiaj
|
||||||
|
odblokuj teraz
|
||||||
|
kryptowaluty inwestycja
|
||||||
|
pożyczka online bez formalności
|
||||||
|
bezpieczna inwestycja
|
||||||
|
zyskaj bezpieczeństwo
|
||||||
|
dodatkowe zabezpieczenie
|
||||||
|
tylko dla nowych użytkowników
|
||||||
|
dodatkowe środki na koncie
|
||||||
|
szybka pożyczka online
|
||||||
|
nagroda czeka na ciebie
|
||||||
|
bezpieczna transakcja
|
||||||
|
potwierdź telefonicznie
|
||||||
|
darmowa karta kredytowa
|
||||||
|
oferta niepowtarzalna
|
||||||
|
łatwy sposób na sukces
|
||||||
|
proste rozwiązanie
|
||||||
|
zainwestuj teraz
|
||||||
|
oferta wyłącznie przez 24 godziny
|
||||||
|
kryptowaluty pożyczka
|
||||||
|
kup teraz
|
||||||
|
natychmiastowa transakcja
|
||||||
|
oferta czasowa
|
||||||
|
błyskawiczne pożyczki
|
||||||
|
wejdź teraz
|
||||||
|
zainwestuj bez ryzyka
|
||||||
|
inwestycje bez formalności
|
||||||
|
oferta specjalna
|
||||||
|
czekają na ciebie
|
||||||
|
inwestycja w przyszłość
|
||||||
|
spróbuj teraz
|
||||||
|
zyskaj dla siebie
|
||||||
|
oferta tylko przez 24 godziny
|
||||||
|
zainwestuj bez opóźnień
|
||||||
|
pożyczka bez weryfikacji
|
||||||
|
sprawdź nasze oferty
|
||||||
|
kliknij tutaj, aby wygrać
|
||||||
|
kryptowaluty bez ryzyka
|
||||||
|
zapłać teraz
|
||||||
|
pożyczka do 10000 zł
|
||||||
|
wygraj teraz
|
||||||
|
zapłać tylko 1 zł
|
||||||
|
tylko teraz
|
||||||
|
oferta dnia
|
||||||
|
kliknij, aby zdobyć bonus
|
||||||
|
darmowe pieniądze online
|
||||||
|
wygraj pożyczkę
|
||||||
|
wygrana czeka
|
||||||
|
zyskaj dla siebie
|
||||||
|
zarejestruj się teraz
|
||||||
|
aktywuj konto
|
||||||
|
darmowy dostęp
|
||||||
|
oferta czasowa
|
||||||
|
bez ryzyka
|
||||||
|
otwórz konto teraz
|
||||||
|
pożyczka na dowolny cel
|
||||||
|
sprawdzona metoda zarobku
|
||||||
|
otrzymaj teraz
|
||||||
|
potwierdź e-mail
|
||||||
|
weryfikacja numeru telefonu
|
||||||
|
płatności w 24h
|
||||||
|
wygraj w konkursie
|
||||||
|
darmowy kredyt
|
||||||
|
sprawdź pożyczki
|
||||||
|
wygodne pożyczki
|
||||||
|
otrzymaj pełny dostęp
|
||||||
|
chciałbyś wybrać gotówkę?
|
||||||
|
płatności online teraz
|
||||||
|
opóźniona weryfikacja
|
||||||
|
nie przegap okazji
|
||||||
|
przyjdź po swoją nagrodę
|
||||||
|
zainwestuj w kryptowaluty
|
||||||
|
inwestuj teraz
|
||||||
|
ograniczone inwestycje
|
||||||
|
profesjonalna pożyczka
|
||||||
|
oferta bez opłat
|
||||||
|
zyskaj ogromną nagrodę
|
||||||
|
przyjdź i wygraj
|
||||||
|
pożyczka z uproszczoną weryfikacją
|
||||||
|
free money
|
||||||
|
get rich now
|
||||||
|
urgent offer
|
||||||
|
limited time offer
|
||||||
|
exclusive offer
|
||||||
|
final offer
|
||||||
|
act now
|
||||||
|
confirm your details
|
||||||
|
account verification
|
||||||
|
confirm phone number
|
||||||
|
check your account
|
||||||
|
account expired
|
||||||
|
urgent account
|
||||||
|
login now
|
||||||
|
won a contest
|
||||||
|
prize waiting for you
|
||||||
|
free gift
|
||||||
|
get millions
|
||||||
|
confirm your email
|
||||||
|
visit our site
|
||||||
|
claim your reward
|
||||||
|
no effort earnings
|
||||||
|
guaranteed investment
|
||||||
|
take part now
|
||||||
|
reward for you
|
||||||
|
free registration
|
||||||
|
special offer
|
||||||
|
free trial
|
||||||
|
receive discount code
|
||||||
|
register now
|
||||||
|
quick cash
|
||||||
|
win now
|
||||||
|
pay now
|
||||||
|
offer just for you
|
||||||
|
click here
|
||||||
|
join now
|
||||||
|
limited access
|
||||||
|
be the first
|
||||||
|
provide your number
|
||||||
|
confirm your details
|
||||||
|
account security
|
||||||
|
instant access
|
||||||
|
you must click
|
||||||
|
thanks for your trust
|
||||||
|
open now
|
||||||
|
key information
|
||||||
|
your account has been suspended
|
||||||
|
account blocked
|
||||||
|
login to unlock
|
||||||
|
loan without verification
|
||||||
|
online loan
|
||||||
|
invest now
|
||||||
|
quick loan
|
||||||
|
get credit now
|
||||||
|
cryptocurrency
|
||||||
|
bitcoin investment
|
||||||
|
withdraw now
|
||||||
|
your bonus
|
||||||
|
profitable investment
|
||||||
|
simple method
|
||||||
|
earn online
|
||||||
|
additional income
|
||||||
|
don’t wait
|
||||||
|
your details are needed
|
||||||
|
verify your details
|
||||||
|
unlock access
|
||||||
|
exclusive offer for you
|
||||||
|
immediate verification
|
||||||
|
automatic profits
|
||||||
|
profits in 24 hours
|
||||||
|
visit our site
|
||||||
|
online payment
|
||||||
|
click to continue
|
||||||
|
verified earnings
|
||||||
|
secure payment
|
||||||
|
earn money easily
|
||||||
|
instant reward
|
||||||
|
quick withdrawal
|
||||||
|
easy money
|
||||||
|
exclusive content
|
||||||
|
limited time access
|
||||||
|
urgent message
|
||||||
|
unmissable offer
|
||||||
|
make money now
|
||||||
|
urgent response required
|
||||||
|
special deal
|
||||||
|
become a winner now
|
||||||
|
limited time deal
|
||||||
|
secure transaction
|
||||||
|
immediate results
|
||||||
|
unlimited bonus
|
||||||
|
enter here now
|
||||||
|
invest without risk
|
||||||
|
simple way to earn
|
||||||
|
online bonus
|
||||||
|
easy profits
|
||||||
|
get started today
|
||||||
|
guaranteed money
|
||||||
|
special promotion
|
||||||
|
withdraw your profits
|
||||||
|
simple solution
|
||||||
|
limited time access
|
||||||
|
sign up now
|
||||||
|
act fast
|
||||||
|
investment opportunity
|
||||||
|
easy way to invest
|
||||||
|
instant payout
|
||||||
|
top opportunity
|
||||||
|
don’t miss this chance
|
||||||
|
confirm now
|
||||||
|
no risk investment
|
||||||
|
get paid now
|
||||||
|
secure online payments
|
||||||
|
win big
|
||||||
|
new offers available
|
||||||
|
get free bonus
|
||||||
|
fast payouts
|
||||||
|
limited time only
|
||||||
|
secure your spot
|
||||||
|
instant confirmation
|
||||||
|
quick profits
|
||||||
|
fast money
|
||||||
|
loan with no verification
|
||||||
|
get rewards
|
||||||
|
get started now
|
||||||
|
confirm email
|
||||||
|
confirm phone number
|
||||||
|
take advantage of this offer
|
||||||
|
offer only available now
|
||||||
|
easy loan
|
||||||
|
verify now
|
||||||
|
check your winnings
|
||||||
|
free loans
|
||||||
|
secure loans
|
||||||
|
fast loans
|
||||||
|
access now
|
||||||
|
no payment required
|
||||||
|
easy registration
|
||||||
|
online payment system
|
||||||
|
earn rewards now
|
||||||
|
your payment is waiting
|
||||||
|
invest in crypto
|
||||||
|
safe investment
|
||||||
|
don’t miss the chance
|
||||||
|
instant payout now
|
||||||
|
get access immediately
|
||||||
|
sign up today
|
||||||
|
free cryptocurrency
|
||||||
|
act quickly
|
||||||
|
unmissable opportunity
|
||||||
|
click to win
|
||||||
|
guaranteed rewards
|
||||||
|
free rewards
|
||||||
|
take action now
|
||||||
|
|
||||||
67
data/scam_numbers.txt
Normal file
67
data/scam_numbers.txt
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
+48795800000
|
||||||
|
+48600123456
|
||||||
|
+48777233444
|
||||||
|
+447700900123
|
||||||
|
+447800000000
|
||||||
|
+447400000000
|
||||||
|
+447500000000
|
||||||
|
+447600000000
|
||||||
|
+447900000000
|
||||||
|
+442080000000
|
||||||
|
+37200000000
|
||||||
|
+420000000000
|
||||||
|
+37000000000
|
||||||
|
+48000000000
|
||||||
|
+375000000000
|
||||||
|
+810000000000
|
||||||
|
+870000000000
|
||||||
|
+880000000000
|
||||||
|
48500123456
|
||||||
|
48501123456
|
||||||
|
48502123456
|
||||||
|
48503123456
|
||||||
|
48504123456
|
||||||
|
48505123456
|
||||||
|
48506123456
|
||||||
|
48507123456
|
||||||
|
48508123456
|
||||||
|
48509123456
|
||||||
|
70700
|
||||||
|
70701
|
||||||
|
70702
|
||||||
|
70800
|
||||||
|
70801
|
||||||
|
70802
|
||||||
|
70900
|
||||||
|
70901
|
||||||
|
70902
|
||||||
|
80000
|
||||||
|
80001
|
||||||
|
80002
|
||||||
|
80100
|
||||||
|
80101
|
||||||
|
80102
|
||||||
|
80200
|
||||||
|
80201
|
||||||
|
80202
|
||||||
|
80300
|
||||||
|
80301
|
||||||
|
80302
|
||||||
|
80400
|
||||||
|
80401
|
||||||
|
80402
|
||||||
|
80500
|
||||||
|
80501
|
||||||
|
80502
|
||||||
|
80600
|
||||||
|
80601
|
||||||
|
80602
|
||||||
|
80700
|
||||||
|
80701
|
||||||
|
80702
|
||||||
|
80800
|
||||||
|
80801
|
||||||
|
80802
|
||||||
|
80900
|
||||||
|
80901
|
||||||
|
80902
|
||||||
7
data/url_shorteners.txt
Normal file
7
data/url_shorteners.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
bit.ly
|
||||||
|
tinyurl.com
|
||||||
|
t.co
|
||||||
|
goo.gl
|
||||||
|
rb.gy
|
||||||
|
is.gd
|
||||||
|
tiny.pl
|
||||||
9
requirements.txt
Normal file
9
requirements.txt
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Flask
|
||||||
|
gunicorn
|
||||||
|
requests
|
||||||
|
transformers
|
||||||
|
torch
|
||||||
|
python-dotenv
|
||||||
|
numpy
|
||||||
|
pytz
|
||||||
|
tzlocal
|
||||||
69
static/css/style.css
Normal file
69
static/css/style.css
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
body {
|
||||||
|
transition: background-color 0.4s, color 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition-theme {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition-theme.dark-mode {
|
||||||
|
background-color: #121212;
|
||||||
|
color: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode .bg-body-tertiary {
|
||||||
|
background-color: #1e1e1e !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode .form-control,
|
||||||
|
.dark-mode .form-label {
|
||||||
|
background-color: #2c2c2c;
|
||||||
|
color: #e9ecef;
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode .form-control::placeholder {
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode .alert-success {
|
||||||
|
background-color: #224422;
|
||||||
|
color: #cfc;
|
||||||
|
border-color: #3d3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode .alert-danger {
|
||||||
|
background-color: #441111;
|
||||||
|
color: #fbb;
|
||||||
|
border-color: #e33;
|
||||||
|
}
|
||||||
|
/* Style dla motywu jasnego (domyślne) */
|
||||||
|
.transition-theme {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #212529;
|
||||||
|
border-color: #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style dla motywu ciemnego */
|
||||||
|
body.dark-mode .transition-theme {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
color: #f8f9fa;
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .list-group-item.transition-theme {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
color: #f8f9fa;
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .text-muted.transition-theme {
|
||||||
|
color: #adb5bd !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .card.transition-theme {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
312
templates/index.html
Normal file
312
templates/index.html
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>AntiScam Pro</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body class="transition-theme">
|
||||||
|
|
||||||
|
<div class="container py-5">
|
||||||
|
<!-- Standard Bootstrap container (responsive) -->
|
||||||
|
<!-- Header with title and buttons - using responsive flexbox -->
|
||||||
|
<!-- d-flex justify-content-between align-items-center is a default horizontal flexbox -->
|
||||||
|
<!-- flex-column-sm-row: stack in column on < sm, row on >= sm -->
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4 flex-column flex-sm-row">
|
||||||
|
<!-- Title - bottom margin on small screens (mb-3), removed on >= sm (mb-sm-0) -->
|
||||||
|
<h1 class="fw-bold mb-3 mb-sm-0">AntiScam Pro</h1>
|
||||||
|
|
||||||
|
<!-- Button container - also stacked on small screens -->
|
||||||
|
<div class="d-flex flex-column flex-sm-row">
|
||||||
|
<!-- Theme button - bottom margin on small, right margin on >= sm -->
|
||||||
|
<button id="toggleTheme" class="btn btn-outline-secondary mb-2 me-sm-2">
|
||||||
|
<i class="fas fa-moon"></i> <span id="theme-btn-text" data-pl="Zmień motyw" data-en="Toggle theme">Toggle theme</span>
|
||||||
|
</button>
|
||||||
|
<!-- Language toggle button -->
|
||||||
|
<button id="toggleLanguage" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-language"></i> <span id="lang-btn-text">PL</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form section -->
|
||||||
|
<form method="POST" class="bg-body-tertiary p-4 rounded shadow-sm">
|
||||||
|
<input type="hidden" name="lang" id="langInput" value="en">
|
||||||
|
<div class="mb-3">
|
||||||
|
<!-- Single textarea instead of 3 inputs -->
|
||||||
|
<label for="single_input" class="form-label" data-pl="Wprowadź dane do analizy:" data-en="Enter data for analysis:">Enter data for analysis:</label>
|
||||||
|
<textarea name="input_data" id="single_input" rows="3" class="form-control"
|
||||||
|
data-pl-placeholder="Wprowadź wiadomość, numer telefonu lub link..."
|
||||||
|
data-en-placeholder="Enter message, phone number, or link..."
|
||||||
|
placeholder="Enter message, phone number, or link..."></textarea>
|
||||||
|
</div>
|
||||||
|
<!-- Submit button -->
|
||||||
|
<button type="submit" class="btn btn-primary" data-pl="Sprawdź" data-en="Check">Check</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Results section -->
|
||||||
|
<div class="mt-4">
|
||||||
|
|
||||||
|
<!-- Message result -->
|
||||||
|
{% if result.message %}
|
||||||
|
<div class="alert {{ 'alert-danger' if result.message.is_suspicious else 'alert-success' }}">
|
||||||
|
<strong data-pl="Wiadomość:" data-en="Message:">Message:</strong>
|
||||||
|
{% if result.message.is_suspicious %}
|
||||||
|
{% if result.message.suspicious_words %}
|
||||||
|
<span data-pl="Podejrzana. Znalezione wyrazy:" data-en="Suspicious. Found words:">Suspicious. Found words:</span>
|
||||||
|
<em>{{ result.message.suspicious_words | join(', ') }}</em>
|
||||||
|
{% endif %}
|
||||||
|
{% if result.message.ai_result %}
|
||||||
|
<p data-pl="Wykryte przez AI:" data-en="Detected by AI:">Detected by AI:</p>
|
||||||
|
<p>{{ result.message.ai_result.label }} ({{ (result.message.ai_result.confidence * 100) | round(1) }}%)</p>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<span data-pl="Brak podejrzanych treści." data-en="No suspicious content.">No suspicious content.</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Phone number result -->
|
||||||
|
{% if result.phone %}
|
||||||
|
<div class="alert {{ 'alert-danger' if result.phone.is_suspicious else 'alert-success' }}">
|
||||||
|
<strong data-pl="Numer telefonu:" data-en="Phone number:">Phone number:</strong>
|
||||||
|
{% if result.phone.is_suspicious %}
|
||||||
|
<span data-pl="Podejrzany numer." data-en="Suspicious number.">Suspicious number.</span>
|
||||||
|
<p data-pl="Powód podejrzenia: Numer w bazie danych podejrzanych numerów." data-en="Reason: Number found in suspicious database.">
|
||||||
|
Reason: Number found in suspicious database.
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<span data-pl="Wygląda na bezpieczny." data-en="Looks safe.">Looks safe.</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Link result section -->
|
||||||
|
{% if result.link is defined and (result.link.is_suspicious or result.link.details) %}
|
||||||
|
|
||||||
|
{% set overall_link_suspicious_display = result.link.is_suspicious %}
|
||||||
|
|
||||||
|
<div class="alert {{ 'alert-danger' if overall_link_suspicious_display else 'alert-success' }}">
|
||||||
|
<strong data-pl="Link:" data-en="Link:">Link:</strong>
|
||||||
|
|
||||||
|
{% if overall_link_suspicious_display %}
|
||||||
|
<span style="color: red;" data-pl="Podejrzany." data-en="Suspicious.">Suspicious.</span>
|
||||||
|
{% else %}
|
||||||
|
<span style="color: green;" data-pl="Brak podejrzanej aktywności." data-en="No suspicious activity detected.">No suspicious activity detected.</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if result.link.details %}
|
||||||
|
<p data-pl="Szczegóły analizy:" data-en="Analysis Details:">Analysis Details:</p>
|
||||||
|
<ul>
|
||||||
|
{% for detail in result.link.details %}
|
||||||
|
<li>
|
||||||
|
<span data-pl="{{ detail.get('data-pl', detail.get('text', '')) }}" data-en="{{ detail.get('data-en', detail.get('text', '')) }}">
|
||||||
|
{{ detail.get('text', 'Unknown analysis detail') }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% elif not overall_link_suspicious_display %}
|
||||||
|
<p data-pl="Brak dodatkowych szczegółów analizy." data-en="No additional analysis details.">No additional analysis details.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if result.link is defined and result.link.is_valid is defined %}
|
||||||
|
<p data-pl="Format URL poprawny: {{ 'Tak' if result.link.is_valid else 'Nie' }}" data-en="URL format valid: {{ 'Yes' if result.link.is_valid else 'No' }}">URL format valid: {{ 'Yes' if result.link.is_valid else 'No' }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% elif result.link is not defined %}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<span data-pl="Podaj dane do analizy." data-en="Provide data for analysis.">Provide data for analysis.</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Info and Statistics section -->
|
||||||
|
<div class="container mt-4">
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card h-100 transition-theme">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title" data-pl="O AntiScam Pro" data-en="About AntiScam Pro">About AntiScam Pro</h2>
|
||||||
|
<p class="card-text" data-pl="AntiScam Pro to zaawansowana aplikacja do wykrywania oszustw..." data-en="AntiScam Pro is an advanced tool for scam detection...">
|
||||||
|
AntiScam Pro is an advanced application for detecting scams, spam, and suspicious links. It uses AI and databases to ensure safety.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 class="card-title" data-pl="Funkcje:" data-en="Features:">Features:</h3>
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
<li class="list-group-item transition-theme" data-pl="Wykrywanie podejrzanych wiadomości tekstowych" data-en="Detects suspicious text messages">Detects suspicious text messages</li>
|
||||||
|
<li class="list-group-item transition-theme" data-pl="Weryfikacja numerów telefonów i linków" data-en="Phone number and link verification">Phone number and link verification</li>
|
||||||
|
<li class="list-group-item transition-theme">
|
||||||
|
<details>
|
||||||
|
<summary data-pl="Integracja z VirusTotal" data-en="Integration with VirusTotal">Integration with VirusTotal</summary>
|
||||||
|
<p class="text-muted mt-2 transition-theme" data-pl="Analiza linków obejmuje skanowanie VirusTotal..." data-en="Link analysis includes VirusTotal scanning if API key is configured and link requires verification.">
|
||||||
|
Link analysis includes VirusTotal scanning if API key is configured and link requires verification.
|
||||||
|
</p>
|
||||||
|
</details>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item transition-theme">
|
||||||
|
<details>
|
||||||
|
<summary data-pl="Obszerne bazy danych" data-en="Extensive databases">Extensive databases</summary>
|
||||||
|
<p class="text-muted mt-2 transition-theme" data-pl="Nasza baza zawiera..." data-en="Our database contains over 186,000 suspicious domains.">
|
||||||
|
Our database contains over 186,000 suspicious domains.
|
||||||
|
</p>
|
||||||
|
</details>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card h-100 transition-theme">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title" data-pl="📊 Statystyki globalne" data-en="📊 Global statistics">📊 Global statistics</h3>
|
||||||
|
<p class="text-muted transition-theme" data-pl="(od uruchomienia serwera)" data-en="(since server start)">(since server start)</p>
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
<li class="list-group-item transition-theme">
|
||||||
|
<span data-pl="Wiadomości sprawdzone:" data-en="Messages checked:">Messages checked:</span>
|
||||||
|
<span class="badge bg-primary">{{ global_stats.messages_checked }}</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item transition-theme">
|
||||||
|
<span data-pl="Numery sprawdzone:" data-en="Phones checked:">Phones checked:</span>
|
||||||
|
<span class="badge bg-primary">{{ global_stats.phones_checked }}</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item transition-theme">
|
||||||
|
<span data-pl="Linki sprawdzone:" data-en="Links checked:">Links checked:</span>
|
||||||
|
<span class="badge bg-primary">{{ global_stats.links_checked }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3 class="card-title mt-4" data-pl="👤 Twoje statystyki" data-en="👤 Your statistics">👤 Your statistics</h3>
|
||||||
|
<p class="text-muted transition-theme" data-pl="(ta sesja)" data-en="(this session)">(this session)</p>
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
<li class="list-group-item transition-theme">
|
||||||
|
<span data-pl="Wiadomości sprawdzone:" data-en="Messages checked:">Messages checked:</span>
|
||||||
|
<span class="badge bg-info">{{ session_stats.messages_checked }}</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item transition-theme">
|
||||||
|
<span data-pl="Numery sprawdzone:" data-en="Phones checked:">Phones checked:</span>
|
||||||
|
<span class="badge bg-info">{{ session_stats.phones_checked }}</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item transition-theme">
|
||||||
|
<span data-pl="Linki sprawdzone:" data-en="Links checked:">Links checked:</span>
|
||||||
|
<span class="badge bg-info">{{ session_stats.links_checked }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- JS Scripts -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const btnTheme = document.getElementById("toggleTheme");
|
||||||
|
const btnLang = document.getElementById("toggleLanguage");
|
||||||
|
const themeText = document.getElementById("theme-btn-text");
|
||||||
|
const langText = document.getElementById("lang-btn-text");
|
||||||
|
const langInput = document.getElementById("langInput");
|
||||||
|
const singleInput = document.getElementById("single_input");
|
||||||
|
|
||||||
|
// User system preference for dark mode
|
||||||
|
const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
|
||||||
|
// Get preferences from localStorage or use system preference
|
||||||
|
let darkMode = localStorage.getItem("darkMode");
|
||||||
|
if (darkMode === null) {
|
||||||
|
darkMode = prefersDarkMode.matches;
|
||||||
|
} else {
|
||||||
|
darkMode = (darkMode === "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default language = English
|
||||||
|
let lang = localStorage.getItem("lang") || "en";
|
||||||
|
|
||||||
|
// Update theme button text and icon
|
||||||
|
function updateThemeButtonText(isDarkMode, currentLang) {
|
||||||
|
const textPl = isDarkMode ? "Jasny motyw" : "Ciemny motyw";
|
||||||
|
const textEn = isDarkMode ? "Light theme" : "Dark theme";
|
||||||
|
if (themeText) {
|
||||||
|
themeText.dataset.pl = textPl;
|
||||||
|
themeText.dataset.en = textEn;
|
||||||
|
themeText.textContent = currentLang === "pl" ? textPl : textEn;
|
||||||
|
}
|
||||||
|
const themeIcon = btnTheme ? btnTheme.querySelector('i') : null;
|
||||||
|
if (themeIcon) {
|
||||||
|
themeIcon.classList.toggle('fa-moon', isDarkMode);
|
||||||
|
themeIcon.classList.toggle('fa-sun', !isDarkMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply theme
|
||||||
|
function applyTheme(isDarkMode) {
|
||||||
|
if (document.body) {
|
||||||
|
document.body.classList.toggle("dark-mode", isDarkMode);
|
||||||
|
updateThemeButtonText(isDarkMode, lang);
|
||||||
|
localStorage.setItem("darkMode", isDarkMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply language + placeholder updates
|
||||||
|
function applyLanguage(language) {
|
||||||
|
document.querySelectorAll("[data-pl]").forEach(el => {
|
||||||
|
if (el.dataset.pl && el.dataset.en) {
|
||||||
|
el.textContent = el.dataset[language];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (singleInput) {
|
||||||
|
singleInput.placeholder = singleInput.dataset[language + '-placeholder'] || singleInput.placeholder;
|
||||||
|
}
|
||||||
|
if (langText) {
|
||||||
|
langText.textContent = language === "pl" ? "EN" : "PL";
|
||||||
|
}
|
||||||
|
if (langInput) {
|
||||||
|
langInput.value = language;
|
||||||
|
}
|
||||||
|
localStorage.setItem("lang", language);
|
||||||
|
updateThemeButtonText(darkMode, language);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial setup
|
||||||
|
applyTheme(darkMode);
|
||||||
|
applyLanguage(lang);
|
||||||
|
|
||||||
|
// Button listeners
|
||||||
|
if (btnTheme) {
|
||||||
|
btnTheme.addEventListener("click", () => {
|
||||||
|
darkMode = !darkMode;
|
||||||
|
applyTheme(darkMode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (btnLang) {
|
||||||
|
btnLang.addEventListener("click", () => {
|
||||||
|
lang = lang === "pl" ? "en" : "pl";
|
||||||
|
applyLanguage(lang);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// System preference listener
|
||||||
|
if (prefersDarkMode) {
|
||||||
|
prefersDarkMode.addListener(function(e) {
|
||||||
|
if (localStorage.getItem("darkMode") === null) {
|
||||||
|
applyTheme(e.matches);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("AntiScam Pro custom JavaScript script finished execution.");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
Reference in New Issue
Block a user