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