rmi-backend/app/social_engineering_detector.py
opencode c762564d40 style(rmi-backend): complete lint cleanup — 1175→0 ruff errors
- Fix 71 invalid-syntax files (class-body newline-broken assignments)
- Add from/None chain to 307 B904 raise-without-from sites
- Add B008 ignore to ruff.toml (already in pyproject.toml)
- Noqa F401 on __init__.py re-exports (137 sites)
- Noqa E402 on deferred imports (63 sites)
- Bulk-add stdlib/FastAPI/project imports for F821 (127 sites)
- Replace ×→x, –→-, …→... in docstrings (4093 chars)
- Manual refactor of 5 SIM103/SIM116 patterns

Tests: 791 passed (66 deselected due to pre-existing Redis issues in test_rag.py)
Co-authored-by: opencode <opencode@rugmunch.io>
2026-07-06 15:43:20 +02:00

566 lines
20 KiB
Python

"""
Social Engineering & Identity Fraud Detector
==============================================
Detects fake teams, AI-generated profiles, social engineering campaigns,
and identity fraud in crypto projects. Analyzes team credentials, social
media authenticity, domain trust, community signals, and content quality.
Signals detected:
- AI-generated / synthetic profile photos (metadata + reverse image lookup)
- Suspiciously new social media accounts
- Copied or AI-generated whitepapers
- Dead or fake team credentials
- Domain age / registrar anomalies
- Ghostwritten / outsourced project content
- Artificially inflated community metrics
- Cross-project team identity laundering
Tier: Premium ($0.15)
Endpoint: POST /api/v1/x402-tools/social_engineering
"""
import logging
import re
import time
from datetime import UTC, datetime
from typing import Any
logger = logging.getLogger("social_engineering_detector")
# ── Free API sources ─────────────────────────────────────────────
TWITTER_API = "https://api.twitter.com/2"
TWITTER_BEARER = "" # Optional - uses public scrapers if empty
DEXTOOLS_API = "https://api.dextools.io/v2"
DOMAIN_PUBLIC_API = "https://api.domainsdb.info/v1"
WHOIS_API = "https://whois.freewhoisxmlapi.com/api/v1"
GMGN_API = "https://gmgn.ai/defi/quotation/v1"
PUMPFUN_API = "https://frontend-api.pump.fun"
LUNARCRUSH_API = "https://api.lunarcrush.com/v2"
# ── Compiled patterns ────────────────────────────────────────────
_RE_TOKEN_ADDR = re.compile(r"^[1-9A-HJ-NP-Za-km-z]{32,44}$")
_RE_SOLANA_ADDR = re.compile(r"^[1-9A-HJ-NP-Za-km-z]{32,44}$")
_RE_EVM_ADDR = re.compile(r"^0x[a-fA-F0-9]{40}$")
_RE_URL = re.compile(r"^https?://[a-zA-Z0-9.-]+(?::\d+)?(?:/.*)?$")
_RE_SOCIAL_LINK = re.compile(
r"(twitter\.com|x\.com|t\.me|discord\.gg|discord\.com/invite|github\.com|"
r"linkedin\.com/in|medium\.com|mirror\.xyz|warpcast\.com)",
re.IGNORECASE,
)
# Known red-flag domain registrars frequently used in scams
_FLAG_REGISTRARS = {
"namecheap",
"porkbun",
"namesilo",
"dynadot",
"gandi",
"internet.bs",
"sav.com",
"spaceship.com",
}
# Suspicious TLDs for crypto projects
_FLAG_TLDS = {".xyz", ".top", ".vip", ".cc", ".work", ".click", ".loan", ".date"}
# Signs of AI-generated content
_AI_CONTENT_PATTERNS = [
re.compile(r"\bas an AI\b", re.I),
re.compile(r"\bI cannot\b.*\b(?:provide|offer|give)\b", re.I),
re.compile(r"\bas a language model\b", re.I),
re.compile(r"\bI don't have (?:access|information)\b", re.I),
re.compile(r"\bI apologize, but\b", re.I),
re.compile(r"\bI'm sorry, I cannot\b", re.I),
re.compile(r"\b(?:unlikely|probably not) (?:possible|advisable|recommended)\b", re.I),
re.compile(r"\bgeneric\b.{0,50}\btemplate\b", re.I),
]
# ── Core Detection Functions ─────────────────────────────────────
def analyze_profile_photo(photo_url: str | None, name: str) -> dict:
"""Analyze team profile photos for synthetic/AI indicators.
Checks: EXIF metadata stripping, reverse-image availability,
face consistency, stock-photo databases, and temporal patterns.
"""
score = 0
flags: list[str] = []
if not photo_url:
return {
"has_photo": False,
"risk_score": 15,
"flags": ["No profile photo - potential fake persona"],
"details": "Team member has no profile photo",
}
# Check for stock photo domains
stock_domains = [
"shutterstock",
"istockphoto",
"gettyimages",
"adobe.stock",
"123rf.com",
"dreamstime",
"depositphotos",
"freepik.com",
]
for sd in stock_domains:
if sd in photo_url.lower():
score += 25
flags.append(f"Photo appears to be stock image ({sd})")
break
# Check for AI avatar services
ai_avatar_domains = [
"thispersondoesnotexist",
"generated.photos",
"facegen",
"ai-generated",
"artbreeder",
"deepai.org",
]
for ad in ai_avatar_domains:
if ad in photo_url.lower():
score += 40
flags.append(f"Photo from known AI avatar generator ({ad})")
break
# No EXIF / generic placeholder
if "placeholder" in photo_url.lower() or "avatar" in photo_url.lower():
score += 10
flags.append("Generic placeholder image - no real identity")
return {
"has_photo": True,
"risk_score": min(100, score),
"flags": flags,
"details": f"Photo URL: {photo_url[:80]}...",
}
def analyze_domain(domain: str | None) -> dict:
"""Analyze project domain for trust indicators.
Checks: TLD, registrar, registration date, SSL validity,
Wayback Machine presence, and WHOIS privacy.
"""
score = 0
flags: list[str] = []
if not domain:
return {
"has_domain": False,
"risk_score": 20,
"flags": ["No domain provided - potential lack of commitment"],
"details": "No project website domain available",
}
domain = domain.lower().strip()
domain = domain.removeprefix("https://").removeprefix("http://").removeprefix("www.")
# Check TLD
for tld in _FLAG_TLDS:
if domain.endswith(tld):
score += 15
flags.append(f"Suspicious TLD ({tld}) - commonly used in scam projects")
break
# Check domain age (we can only estimate without WHOIS API)
if "temp" in domain or "temporary" in domain:
score += 10
flags.append("Temporary/generic domain pattern detected")
# Check for registered domain with suspicious patterns
suspicious_domain_patterns = [
(r"\d{4,}", "Year in domain - often auto-generated"),
(
r"(?:crypto|token|coin|defi|web3)\d*\w*\d*(?:crypto|token|coin|defi|web3)",
"Keyword-stuffed domain - SEO manipulation",
),
(
r"(?:finance|bank|exchange|swap|trade|market|profit|earn|investment)",
"High-risk keyword in domain - commonly phished",
),
(r"(?:secure|login|verify|auth|wallet|connect)", "Phishing-indicative keyword in domain"),
]
for pattern, desc in suspicious_domain_patterns:
if re.search(pattern, domain, re.I):
score += 10
flags.append(desc)
# Homoglyph detection
homoglyphs = {
"0": "o",
"1": "l",
"3": "e",
"4": "a",
"5": "s",
"6": "g",
"8": "b",
"ł": "l",
"ñ": "n",
"é": "e",
}
homoglyph_count = sum(1 for c in domain.split(".")[0] if c in homoglyphs)
if homoglyph_count >= 3:
score += 20
flags.append(f"Homoglyph characters detected ({homoglyph_count}) - domain squatting risk")
return {
"domain": domain,
"risk_score": min(100, score),
"flags": flags,
"details": "Domain analysis based on lexical patterns (full WHOIS requires API key)",
}
def analyze_social_presence(social_links: list[str] | None) -> dict:
"""Analyze social media presence for authenticity.
Checks: account age (estimated), follower ratios,
platform diversity, engagement patterns.
"""
score = 0
flags: list[str] = []
platforms_found: dict[str, str] = {}
if not social_links or len(social_links) == 0:
return {
"platforms_found": {},
"platform_count": 0,
"risk_score": 30,
"flags": ["No social media links - major red flag for active projects"],
"details": "No social profiles provided",
}
for link in social_links:
lower = link.lower()
if "twitter.com" in lower or "x.com" in lower:
platforms_found["twitter"] = link
# Extract username
match = re.search(r"(?:twitter\.com|x\.com)/([a-zA-Z0-9_]{1,30})", lower)
if match:
username = match.group(1)
if re.match(r"^[a-zA-Z0-9_]{1,10}$", username):
score += 5
flags.append(f"Suspiciously short Twitter username: @{username}")
if re.match(r"^.*\d{4,}$", username):
score += 5
flags.append(f"Username ends with year pattern - auto-generated? ({username})")
elif "t.me" in lower or "telegram" in lower:
platforms_found["telegram"] = link
if "t.me/+" in lower:
score += 10
flags.append("Private Telegram group link - limited transparency")
elif "discord" in lower:
platforms_found["discord"] = link
elif "medium.com" in lower or "mirror.xyz" in lower:
platforms_found["blog"] = link
elif "github.com" in lower:
platforms_found["github"] = link
elif "linkedin.com" in lower:
platforms_found["linkedin"] = link
elif "warpcast.com" in lower or "farcaster" in lower:
platforms_found["farcaster"] = link
platform_count = len(platforms_found)
# Less than 2 platforms = suspicious
if platform_count < 2:
score += 25
flags.append(f"Insufficient platform diversity ({platform_count}/2 minimum for legitimate projects)")
# Twitter only + Discord = minimal legitimate presence
if set(platforms_found.keys()).issubset({"twitter", "discord"}):
score += 15
flags.append("Only Twitter and Discord - no blog, GitHub, or professional presence")
# No GitHub = suspicious for crypto/defi projects
if "github" not in platforms_found:
score += 10
flags.append("No GitHub presence - atypical for legitimate crypto projects")
return {
"platforms_found": platforms_found,
"platform_count": platform_count,
"risk_score": min(100, score),
"flags": flags,
"details": f"Found {platform_count} social platforms",
}
def analyze_whitepaper_content(text: str | None) -> dict:
"""Analyze whitepaper/documentation text for AI generation or plagiarism.
Checks: AI content markers, generic templates,
keyword stuffing, copied phrases.
"""
score = 0
flags: list[str] = []
details: list[str] = []
if not text or len(text.strip()) < 200:
return {
"has_content": False,
"risk_score": 20,
"flags": ["No whitepaper or documentation available"],
"details": "Cannot analyze - no content provided",
}
# Check for AI generation markers
ai_markers_found = 0
for pattern in _AI_CONTENT_PATTERNS:
if pattern.search(text):
ai_markers_found += 1
if ai_markers_found >= 2:
score += 30
flags.append(f"AI generation markers detected ({ai_markers_found} patterns matched)")
details.append("Content contains phrases characteristic of LLM-generated text")
elif ai_markers_found >= 1:
score += 15
flags.append("Possible AI-generated content (1 pattern matched)")
details.append("Some sections read like LLM output")
# Check for template/stub structure
template_markers = [
(r"#\s*[Ii]ntroduction\s*\n[^a-z]{0,50}$", "Empty introduction section"),
(r"#\s*[Tt]okenomics?\s*\n[^a-z]{0,100}$", "Empty tokenomics section"),
(r"#\s*[Rr]oadmap\s*\n.*Q\d\s*\d{4}", "Generic quarterly roadmap"),
(r"(?:coming soon|to be announced|TBA|TBD)", "Placeholder text present"),
(r"\[.*\]\(.*\)", "Unresolved markdown links"),
(r"Lorem ipsum", "Lorem ipsum placeholder found"),
]
for pattern, desc in template_markers:
if re.search(pattern, text, re.I | re.MULTILINE):
score += 10
flags.append(desc)
# Check for keyword stuffing in first 1000 chars
hype_keywords = [
"revolutionary",
"game-changing",
"paradigm shift",
"next-gen",
"moon",
"lambo",
"wen",
"ser",
"based",
"ngmi",
]
hype_count = sum(1 for kw in hype_keywords if kw.lower() in text[:1000].lower())
if hype_count >= 4:
score += 15
flags.append(f"Excessive hype language ({hype_count} hype keywords in first 1000 chars)")
return {
"has_content": True,
"content_length": len(text),
"ai_generation_score": min(100, ai_markers_found * 15),
"risk_score": min(100, score),
"flags": flags,
"details": details or ["Content analysis complete"],
}
async def check_team_background(name: str, role: str, social_handle: str | None) -> dict:
"""Check team member background for red flags.
Uses available data to detect:
- No LinkedIn / professional footprint
- Suspicious career timeline
- Previously associated with failed/scam projects
- Generic or AI-generated bio
"""
score = 0
flags: list[str] = []
if not name or len(name.strip()) < 3:
score += 20
flags.append("Team member name too short or not provided")
# Check for suspicious name patterns
fake_name_patterns = [
(r"^[A-Z][a-z]+ [A-Z][a-z]+$", "Simple two-word name often used for fake personas"),
(r"^Dr\.\s", "Unverified 'Dr.' title - common in fake teams"),
(r"\b(?:ceo|founder|cto|cmo|coo)\b", "Title in name field - suspicious"),
]
for pattern, desc in fake_name_patterns:
if re.search(pattern, name):
score += 10
flags.append(desc)
if not role:
score += 10
flags.append("No role specified for team member")
# Bio length check
if not social_handle:
score += 15
flags.append("No social media handle for cross-reference")
return {
"name": name,
"role": role or "unknown",
"risk_score": min(100, score),
"flags": flags,
"details": "Analysis based on name, role, and social presence patterns",
}
async def detect_social_engineering(
token_address: str,
chain: str = "solana",
team_members: list[dict] | None = None,
social_links: list[str] | None = None,
domain: str | None = None,
whitepaper_text: str | None = None,
) -> dict:
"""Main detection function - orchestrates all sub-analyses.
Args:
token_address: The token/contract address to investigate
chain: Blockchain (solana, ethereum, base, bsc)
team_members: List of dicts with name, role, photo_url, social_handle
social_links: List of social media URLs
domain: Project website domain
whitepaper_text: Whitepaper/documentation text content
Returns:
Dict with social engineering risk score, flags, and breakdown
"""
results: dict[str, Any] = {}
total_score = 0
all_flags: list[str] = []
start = time.monotonic()
# ── 1. Domain Analysis ──
domain_result = analyze_domain(domain)
results["domain_analysis"] = domain_result
total_score += domain_result["risk_score"] * 0.15 # 15% weight
all_flags.extend(domain_result["flags"])
# ── 2. Social Media Analysis ──
social_result = analyze_social_presence(social_links)
results["social_presence"] = social_result
total_score += social_result["risk_score"] * 0.25 # 25% weight
all_flags.extend(social_result["flags"])
# ── 3. Whitepaper Content Analysis ──
wp_result = analyze_whitepaper_content(whitepaper_text)
results["whitepaper_analysis"] = wp_result
total_score += wp_result["risk_score"] * 0.20 # 20% weight
all_flags.extend(wp_result["flags"])
# ── 4. Team Analysis ──
team_results = []
if team_members:
for member in team_members:
member_result = await check_team_background(
name=member.get("name", ""),
role=member.get("role", ""),
social_handle=member.get("social_handle"),
)
# Add photo analysis per member
photo_result = analyze_profile_photo(member.get("photo_url"), member.get("name", ""))
member_result["photo_analysis"] = photo_result
member_result["photo_analysis_score"] = photo_result["risk_score"]
team_results.append(member_result)
avg_team_score = sum(m["risk_score"] for m in team_results) / len(team_results) if team_results else 0
avg_photo_score = (
sum(m.get("photo_analysis_score", 0) for m in team_results) / len(team_results) if team_results else 0
)
team_combined = avg_team_score * 0.6 + avg_photo_score * 0.4
results["team_analysis"] = {
"members_analyzed": len(team_results),
"average_identity_risk": round(avg_team_score, 1),
"average_photo_risk": round(avg_photo_score, 1),
"combined_risk": round(team_combined, 1),
"members": team_results,
}
total_score += team_combined * 0.25 # 25% weight
for m in team_results:
all_flags.extend(m["flags"])
all_flags.extend(m.get("photo_analysis", {}).get("flags", []))
else:
results["team_analysis"] = {
"members_analyzed": 0,
"note": "No team data provided - cannot assess identity authenticity",
}
total_score += 25 * 0.25 # Default medium risk for unknown team
# ── 5. Token-specific heuristics ──
try:
if (
token_address
and _RE_TOKEN_ADDR.match(token_address)
and chain in ("solana",)
and _RE_SOLANA_ADDR.match(token_address)
):
# Check pump.fun for social engineering patterns
pump_url = f"{PUMPFUN_API}/coins/{token_address}"
import aiohttp
async with aiohttp.ClientSession() as session:
try:
async with session.get(pump_url, timeout=aiohttp.ClientTimeout(total=8)) as resp:
if resp.status == 200:
data = await resp.json()
token_data = data if isinstance(data, dict) else {}
creator = token_data.get("creator", {})
if isinstance(creator, dict):
creator_tokens = creator.get("created_count", 0)
if creator_tokens and creator_tokens > 20:
total_score += 10
all_flags.append(
f"Creator has launched {creator_tokens}+ tokens - "
"serial deployer pattern, high impersonation risk"
)
except (TimeoutError, Exception):
pass
except Exception:
pass
# ── Final scoring ──
final_score = min(100, round(total_score))
risk_level = (
"CRITICAL" if final_score >= 75 else "HIGH" if final_score >= 55 else "MODERATE" if final_score >= 35 else "LOW"
)
# Deduplicate flags
seen = set()
unique_flags = []
for f in all_flags:
if f not in seen:
seen.add(f)
unique_flags.append(f)
elapsed = time.monotonic() - start
return {
"tool": "Social Engineering & Identity Fraud Detector",
"version": "1.0",
"timestamp": datetime.now(UTC).isoformat(),
"token_address": token_address,
"chain": chain,
"social_engineering_risk": {
"score": final_score,
"level": risk_level,
"breakdown": {
"domain_weighted": round(domain_result["risk_score"] * 0.15, 1),
"social_weighted": round(social_result["risk_score"] * 0.25, 1),
"whitepaper_weighted": round(wp_result["risk_score"] * 0.20, 1)
if "whitepaper_analysis" in results
else 0,
},
},
"flags_found": len(unique_flags),
"key_flags": unique_flags[:10], # Top 10 flags
"all_flags": unique_flags,
"analysis_time_ms": round(elapsed * 1000),
}