2026-04-03 16:37:23 +00:00

1721 lines
71 KiB
TypeScript

// ToolsDashboard.tsx — full file (focused single-tool view) — copy/paste ready
import React, { useRef, useState, useEffect, useMemo } from 'react';
import {
Shield, Lock, Mail, Link2, Clock, ClipboardCheck,
Eye, EyeOff, AlertTriangle, CheckCircle, XCircle, Info, FileText, Search, Camera, Download
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';
import { Progress } from '@/components/ui/progress';
import jsQR from 'jsqr';
type URLCheckResult = { status: 'Safe' | 'Suspicious' | 'Dangerous'; issues: string[] };
const checkUrlString = (input: string): URLCheckResult => {
const issues: string[] = [];
const raw = (input || '').trim();
if (!raw) return { status: 'Safe', issues: [] };
const lower = raw.toLowerCase();
let parsed: URL | null = null;
try { parsed = new URL(raw); } catch {
try { parsed = new URL('https://' + raw); } catch { parsed = null; }
}
const hostname = parsed?.hostname?.toLowerCase() || '';
const pathAndQuery = parsed ? ((parsed.pathname || '') + (parsed.search || '')).toLowerCase() : lower;
if (parsed && parsed.protocol === 'http:') {
issues.push('Insecure protocol (HTTP instead of HTTPS).');
}
if (/\.(exe|apk|bat|scr|dll|msi|jar|vbs|cmd)(?:$|\?)/i.test(pathAndQuery) || /\.(exe|apk|bat|scr|dll|msi|jar|vbs|cmd)$/i.test(lower)) {
issues.push('Contains potentially harmful file extension - DO NOT DOWNLOAD.');
}
const typoPatterns = [
{ pattern: 'gooogle', real: 'google' },
{ pattern: 'facebok', real: 'facebook' },
{ pattern: 'instgram', real: 'instagram' },
{ pattern: 'twiter', real: 'twitter' },
{ pattern: 'amazom', real: 'amazon' },
{ pattern: 'paypa1', real: 'paypal' },
{ pattern: 'linkedln', real: 'linkedin' },
];
typoPatterns.forEach(({ pattern, real }) => {
if ((hostname && hostname.includes(pattern)) || lower.includes(pattern)) {
issues.push(`Possible typosquatting: "${pattern}" looks like "${real}".`);
}
});
if (/[0-9]{10,}/.test(pathAndQuery) || /[a-z]{20,}/.test(pathAndQuery) || /[A-Z0-9_-]{25,}/.test(raw)) {
issues.push('Contains long random-looking token/ID (common in phishing or short-lived blobs).');
}
const suspiciousTLDs = ['xyz', 'top', 'club', 'work', 'tk', 'ml', 'ga', 'cf'];
const tld = (hostname.split('.').slice(-1)[0] || '').toLowerCase();
if (tld && suspiciousTLDs.includes(tld)) {
issues.push(`Uses suspicious top-level domain (.${tld}).`);
}
if (/(^|[\W_])(login|signin|verify|secure|account[-_]?verify|update[-_]?payment|auth|confirm)([\W_]|$)/i.test(lower)) {
issues.push('Contains login/verify/auth keywords commonly used in phishing URLs.');
}
if (/\b\d{1,3}(\.\d{1,3}){3}\b/.test(hostname || raw)) {
issues.push('Contains raw IP address (phishing hosts sometimes use IPs).');
}
if (issues.some(i => i.includes('DO NOT DOWNLOAD') || i.includes('Insecure protocol'))) {
return { status: 'Dangerous', issues };
}
if (issues.length >= 2) return { status: 'Dangerous', issues };
if (issues.length === 1) return { status: 'Suspicious', issues };
return { status: 'Safe', issues: [] };
};
const extractUrlsFromText = (text: string): string[] => {
if (!text) return [];
const urlRegex = /https?:\/\/[^\s"'<>]+|www\.[^\s"'<>]+/gi;
const matches = text.match(urlRegex) || [];
return Array.from(new Set(matches.map(m => m.startsWith('www.') ? 'http://' + m : m)));
};
const analyzeUrls = (urls: string[]) => {
const details: { url: string; result: URLCheckResult }[] = [];
let overall: URLCheckResult['status'] = 'Safe';
for (const u of urls) {
const r = checkUrlString(u);
details.push({ url: u, result: r });
if (r.status === 'Dangerous') overall = 'Dangerous';
else if (r.status === 'Suspicious' && overall !== 'Dangerous') overall = 'Suspicious';
}
return { overall, details };
};
interface Tool {
id: string;
title: string;
icon: React.ElementType;
description: string;
color: string;
}
const tools: Tool[] = [
{ id: 'password-strength', title: 'Password Strength Analyzer', icon: Lock, description: 'Check password strength, entropy, and crack time', color: 'primary' },
{ id: 'phishing-detector', title: 'Email Phishing Detector', icon: Mail, description: 'Detect suspicious emails and phishing attempts', color: 'secondary' },
{ id: 'url-checker', title: 'URL / File Risk Checker', icon: Link2, description: 'Analyze URLs for security risks', color: 'accent' },
{ id: 'encrypt-text', title: 'Encrypt Text Tool', icon: Shield, description: 'Encrypt your sensitive text', color: 'primary' },
{ id: 'decrypt-text', title: 'Decrypt Text Tool', icon: Eye, description: 'Decrypt encrypted messages', color: 'secondary' },
{ id: 'stego-encode', title: 'Image Steganography — Encode', icon: FileText, description: 'Hide messages inside PNG images (LSB)', color: 'accent' },
{ id: 'stego-decode', title: 'Image Steganography — Decode', icon: Search, description: 'Extract hidden messages from PNG images (and detect QR)', color: 'primary' },
{ id: 'qr-scanner', title: 'QR Code Security Scanner', icon: Camera, description: 'Scan QR and check URLs for safety', color: 'secondary' },
{ id: 'breach-checker', title: 'Local Breach Checker', icon: AlertTriangle, description: 'Check Gmail/phone against a local simulated breach dataset', color: 'accent' },
];
const ToolsDashboard: React.FC = () => {
const [activeTool, setActiveTool] = useState<string | null>(null);
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') setActiveTool(null);
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, []);
return (
<div className="min-h-screen py-8 px-4">
<div className="container mx-auto max-w-7xl">
<div className="text-center mb-12">
<h1 className="font-display text-4xl lg:text-5xl font-bold text-gradient-primary mb-4">
Cyber Security Tools
</h1>
<p className="text-muted-foreground max-w-2xl mx-auto">
Powerful utilities to analyze, protect, and educate. All tools run locally for your privacy.
</p>
</div>
{!activeTool && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{tools.map((tool) => (
<button
key={tool.id}
type="button"
data-tool-id={tool.id}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setActiveTool(tool.id);
}}
className={cn(
"glass-card-hover p-6 text-left transition-all duration-300",
)}
>
<div className={cn(
"w-12 h-12 rounded-lg flex items-center justify-center mb-4",
tool.color === 'primary' && "bg-primary/20 text-primary",
tool.color === 'secondary' && "bg-secondary/20 text-secondary",
tool.color === 'accent' && "bg-accent/20 text-accent",
)}>
<tool.icon className="w-6 h-6" />
</div>
<h3 className="font-display font-semibold text-lg mb-2">{tool.title}</h3>
<p className="text-sm text-muted-foreground">{tool.description}</p>
</button>
))}
</div>
)}
{activeTool && (
<div className="mt-8 glass-card p-8 animate-fade-in-up relative">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<button
type="button"
onClick={(e) => { e.preventDefault(); e.stopPropagation(); setActiveTool(null); }}
className="inline-flex items-center gap-2 px-3 py-1 rounded bg-muted/10 hover:bg-muted/20"
>
Back
</button>
<h2 className="font-display text-2xl font-bold">
{tools.find(t => t.id === activeTool)?.title ?? 'Tool'}
</h2>
</div>
<button
type="button"
aria-label="Close tool panel"
onClick={(e) => { e.preventDefault(); e.stopPropagation(); setActiveTool(null); }}
className="px-3 py-1 rounded bg-muted/10 hover:bg-muted/20"
>
Close
</button>
</div>
<ToolPanel toolId={activeTool} />
</div>
)}
</div>
</div>
);
};
interface ToolPanelProps {
toolId: string;
}
const ToolPanel: React.FC<ToolPanelProps> = ({ toolId }) => {
switch (toolId) {
case 'password-strength':
return <PasswordStrengthTool />;
case 'phishing-detector':
return <PhishingDetectorTool />;
case 'url-checker':
return <URLCheckerTool />;
case 'encrypt-text':
return <EncryptTextTool />;
case 'decrypt-text':
return <DecryptTextTool />;
case 'stego-encode':
return <StegoEncodeTool />;
case 'stego-decode':
return <StegoDecodeTool />;
case 'qr-scanner':
return <QRScannerTool />;
case 'breach-checker':
return <BreachCheckerLocal />;
default:
return null;
}
};
/* ---------- Password / Phishing / URL / Encrypt / Decrypt tools (full implementations) ---------- */
const PasswordStrengthTool: React.FC = () => {
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const hasUpper = /[A-Z]/.test(password);
const hasLower = /[a-z]/.test(password);
const hasNumber = /[0-9]/.test(password);
const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(password);
const hasLength = password.length >= 8;
const analyzePassword = () => {
const length = password.length;
const charset = (hasUpper ? 26 : 0) + (hasLower ? 26 : 0) + (hasNumber ? 10 : 0) + (hasSpecial ? 32 : 0);
const entropy = length * Math.log2(charset || 1);
let strength = 0;
if (length >= 8) strength++;
if (length >= 12) strength++;
if (hasUpper && hasLower) strength++;
if (hasNumber) strength++;
if (hasSpecial) strength++;
const crackTimes = ['Instant', 'Seconds', 'Minutes', 'Hours', 'Days', 'Years', 'Centuries'];
const crackTimeIndex = Math.min(Math.floor(entropy / 10), crackTimes.length - 1);
return { strength, entropy: entropy.toFixed(2), crackTime: crackTimes[crackTimeIndex] };
};
const analysis = password ? analyzePassword() : null;
return (
<div className="space-y-6">
<div className="flex items-center gap-3 mb-4">
<Lock className="w-6 h-6 text-primary" />
<h2 className="font-display text-2xl font-bold">Password Strength Analyzer</h2>
</div>
<div className="p-4 bg-muted/30 rounded-lg">
<p className="text-sm text-muted-foreground mb-2">
<Info className="w-4 h-4 inline mr-1" />
Enter a password to analyze its strength, entropy, and estimated crack time.
</p>
</div>
<div className="relative">
<Input
type={showPassword ? 'text' : 'password'}
placeholder="Enter password to analyze..."
value={password}
onChange={(e) => setPassword(e.target.value)}
className="pr-12"
/>
<button
onClick={() => setShowPassword(!showPassword)}
className="absolute right-4 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
{analysis && (
<div className="space-y-4 animate-fade-in-up">
<div className="p-4 bg-muted/30 rounded-lg">
<p className="text-sm text-muted-foreground mb-2">Requirements</p>
<ul className="grid grid-cols-2 gap-y-2 gap-x-4 list-none p-0 m-0">
<li className={hasLength ? "text-green-400" : "text-red-400"}>8+ characters</li>
<li className={hasUpper ? "text-green-400" : "text-red-400"}>Uppercase</li>
<li className={hasLower ? "text-green-400" : "text-red-400"}>Lowercase</li>
<li className={hasNumber ? "text-green-400" : "text-red-400"}>Number</li>
<li className={hasSpecial ? "text-green-400" : "text-red-400"}>Special char</li>
</ul>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="p-4 bg-muted/30 rounded-lg">
<p className="text-sm text-muted-foreground mb-2">Strength</p>
<div className="flex items-center gap-2">
<Progress value={(analysis.strength / 5) * 100} className="h-3" />
<span className="font-display font-bold">{analysis.strength}/5</span>
</div>
</div>
<div className="p-4 bg-muted/30 rounded-lg">
<p className="text-sm text-muted-foreground mb-2">Entropy</p>
<p className="font-display text-xl font-bold text-accent">{analysis.entropy} bits</p>
</div>
<div className="p-4 bg-muted/30 rounded-lg">
<p className="text-sm text-muted-foreground mb-2">Crack Time (est.)</p>
<p className="font-display text-xl font-bold text-primary">{analysis.crackTime}</p>
</div>
</div>
</div>
)}
</div>
);
};
const PhishingDetectorTool: React.FC = () => {
const [emailContent, setEmailContent] = useState('');
const [result, setResult] = useState<{ status: string; issues: string[] } | null>(null);
const analyzeEmail = () => {
const issues: string[] = [];
const content = emailContent.toLowerCase();
const urgentWords = ['urgent', 'otp', 'upload', 'personal', 'click', 'information', 'immediately', 'act now', 'limited time', 'expires', 'suspended', 'verify now'];
if (urgentWords.some(word => content.includes(word))) {
issues.push('Contains urgent/pressure language');
}
const ctaPatterns = ['click here', 'verify your account', 'update your payment', 'confirm your', 'click to'];
if (ctaPatterns.some(p => content.includes(p))) {
issues.push('Contains suspicious call-to-action phrases');
}
const dangerousWords = ['password', 'personal', 'ssn', 'social security', 'bank account', 'credit card'];
if (dangerousWords.some(word => content.includes(word))) {
issues.push('Requests sensitive information');
}
if (content.includes('dear customer') || content.includes('dear user') || content.includes('sir/madam')) {
issues.push('Uses generic greeting (potential red flag)');
}
let status = 'Safe';
if (issues.length >= 3) status = 'Dangerous';
else if (issues.length >= 1) status = 'Suspicious';
setResult({ status, issues });
};
return (
<div className="space-y-6">
<div className="flex items-center gap-3 mb-4">
<Mail className="w-6 h-6 text-secondary" />
<h2 className="font-display text-2xl font-bold">Email Phishing Detector</h2>
</div>
<div className="p-4 bg-muted/30 rounded-lg">
<p className="text-sm text-muted-foreground">
<Info className="w-4 h-4 inline mr-1" />
Paste email content to detect phishing indicators: urgent language, suspicious links, and dangerous requests.
</p>
</div>
<Textarea
placeholder="Paste email content here..."
value={emailContent}
onChange={(e) => setEmailContent(e.target.value)}
rows={6}
/>
<Button variant="neon" onClick={analyzeEmail} disabled={!emailContent.trim()}>
Analyze Email
</Button>
{result && (
<div className={cn(
"p-6 rounded-lg animate-fade-in-up",
result.status === 'Safe' && "bg-green-500/10 border border-green-500/30",
result.status === 'Suspicious' && "bg-yellow-500/10 border border-yellow-500/30",
result.status === 'Dangerous' && "bg-destructive/10 border border-destructive/30",
)}>
<div className="flex items-center gap-3 mb-4">
{result.status === 'Safe' && <CheckCircle className="w-6 h-6 text-green-500" />}
{result.status === 'Suspicious' && <AlertTriangle className="w-6 h-6 text-yellow-500" />}
{result.status === 'Dangerous' && <XCircle className="w-6 h-6 text-destructive" />}
<span className="font-display text-xl font-bold">{result.status}</span>
</div>
{result.issues.length > 0 && (
<ul className="space-y-2">
{result.issues.map((issue, i) => (
<li key={i} className="text-sm text-muted-foreground flex items-start gap-2">
<AlertTriangle className="w-4 h-4 mt-0.5 shrink-0" />
{issue}
</li>
))}
</ul>
)}
{result.issues.length === 0 && (
<p className="text-sm text-muted-foreground">No suspicious indicators detected.</p>
)}
</div>
)}
</div>
);
};
const URLCheckerTool: React.FC = () => {
const [url, setUrl] = useState('');
const [result, setResult] = useState<URLCheckResult | null>(null);
const checkURL = () => {
const res = checkUrlString(url);
setResult(res);
};
return (
<div className="space-y-6">
<div className="flex items-center gap-3 mb-4">
<Link2 className="w-6 h-6 text-accent" />
<h2 className="font-display text-2xl font-bold">URL / File Risk Checker</h2>
</div>
<div className="p-4 bg-muted/30 rounded-lg">
<p className="text-sm text-muted-foreground">
<Info className="w-4 h-4 inline mr-1" />
Enter a URL or filename to check for insecure protocols, typosquatting, suspicious patterns, and harmful file extensions.
</p>
</div>
<Input
placeholder="Enter URL or filename to analyze (e.g., https://example.com or file.exe)"
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
<Button variant="neon-blue" onClick={checkURL} disabled={!url.trim()}>
Check URL / File
</Button>
{result && (
<div className={cn(
"p-6 rounded-lg animate-fade-in-up",
result.status === 'Safe' && "bg-green-500/10 border border-green-500/30",
result.status === 'Suspicious' && "bg-yellow-500/10 border border-yellow-500/30",
result.status === 'Dangerous' && "bg-destructive/10 border border-destructive/30",
)}>
<div className="flex items-center gap-3 mb-4">
{result.status === 'Safe' && <CheckCircle className="w-6 h-6 text-green-500" />}
{result.status === 'Suspicious' && <AlertTriangle className="w-6 h-6 text-yellow-500" />}
{result.status === 'Dangerous' && <XCircle className="w-6 h-6 text-destructive" />}
<span className="font-display text-xl font-bold">{result.status}</span>
</div>
{result.issues.length > 0 ? (
<ul className="space-y-2">
{result.issues.map((issue, i) => (
<li key={i} className="text-sm text-muted-foreground flex items-start gap-2">
<AlertTriangle className="w-4 h-4 mt-0.5 shrink-0 text-yellow-500" />
{issue}
</li>
))}
</ul>
) : (
<p className="text-sm text-green-500">No security issues detected with this URL / file.</p>
)}
</div>
)}
</div>
);
};
const EncryptTextTool: React.FC = () => {
const [text, setText] = useState('');
const [encrypted, setEncrypted] = useState('');
const encrypt = () => {
const base64 = btoa(text);
const shuffled = base64.split('').reverse().join('');
setEncrypted(`CX${shuffled}XC`);
};
return (
<div className="space-y-6">
<div className="flex items-center gap-3 mb-4">
<Shield className="w-6 h-6 text-primary" />
<h2 className="font-display text-2xl font-bold">Encrypt Text Tool</h2>
</div>
<div className="p-4 bg-muted/30 rounded-lg">
<p className="text-sm text-muted-foreground">
<Info className="w-4 h-4 inline mr-1" />
Enter text to encrypt. This is a simulation for educational purposes.
</p>
</div>
<Textarea
placeholder="Enter text to encrypt..."
value={text}
onChange={(e) => setText(e.target.value)}
rows={4}
/>
<Button variant="neon" onClick={encrypt} disabled={!text.trim()}>
Encrypt Text
</Button>
{encrypted && (
<div className="p-4 bg-muted/30 rounded-lg font-mono text-sm break-all animate-fade-in-up">
<p className="text-muted-foreground mb-2">Encrypted Output:</p>
<p className="text-accent">{encrypted}</p>
</div>
)}
</div>
);
};
const DecryptTextTool: React.FC = () => {
const [encrypted, setEncrypted] = useState('');
const [decrypted, setDecrypted] = useState('');
const decrypt = () => {
try {
const cleaned = encrypted.replace(/^CX/, '').replace(/XC$/, '');
const reversed = cleaned.split('').reverse().join('');
const decoded = atob(reversed);
setDecrypted(decoded);
} catch {
setDecrypted('Error: Invalid encrypted text format');
}
};
return (
<div className="space-y-6">
<div className="flex items-center gap-3 mb-4">
<Eye className="w-6 h-6 text-secondary" />
<h2 className="font-display text-2xl font-bold">Decrypt Text Tool</h2>
</div>
<div className="p-4 bg-muted/30 rounded-lg">
<p className="text-sm text-muted-foreground">
<Info className="w-4 h-4 inline mr-1" />
Paste encrypted text from our Encrypt tool to decrypt it.
</p>
</div>
<Textarea
placeholder="Paste encrypted text..."
value={encrypted}
onChange={(e) => setEncrypted(e.target.value)}
rows={4}
/>
<Button variant="neon-pink" onClick={decrypt} disabled={!encrypted.trim()}>
Decrypt Text
</Button>
{decrypted && (
<div className="p-4 bg-muted/30 rounded-lg animate-fade-in-up">
<p className="text-muted-foreground mb-2">Decrypted Output:</p>
<p className="text-foreground">{decrypted}</p>
</div>
)}
</div>
);
};
/* ---------------- Updated StegoEncodeTool (minimal filename/link heuristics) ---------------- */
const StegoEncodeTool: React.FC = () => {
const fileRef = useRef<HTMLInputElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [message, setMessage] = useState('');
const [imageName, setImageName] = useState<string | null>(null);
const [info, setInfo] = useState<string | null>(null);
const [capacity, setCapacity] = useState<number | null>(null);
const [safetyStatus, setSafetyStatus] = useState<URLCheckResult | null>(null);
const toBits = (bytes: Uint8Array) => {
const bits: number[] = [];
for (let b of bytes) {
for (let i = 7; i >= 0; i--) bits.push((b >> i) & 1);
}
return bits;
};
const computeCapacity = (width: number, height: number) => {
const totalLSB = width * height * 3;
const usable = totalLSB - 32;
return Math.floor(usable / 8);
};
const analyzeFilename = (name: string | null) => {
if (!name) return null;
const issues: string[] = [];
const lower = name.toLowerCase();
if (/\.(exe|apk|bat|scr|dll|msi|jar|vbs|cmd)$/.test(lower)) {
issues.push('Filename uses executable/installer extension — risky.');
}
if (/\.png\.exe$/.test(lower) || (/\.[^\.]{1,4}\.[a-z]{2,4}$/.test(lower) && lower.split('.').length > 2)) {
issues.push('Double-extension in filename (possible trick).');
}
if (issues.length === 0) return null;
return { status: issues.length > 0 ? 'Suspicious' : 'Safe', issues } as URLCheckResult;
};
const handleImageLoad = async (file?: File) => {
setInfo(null); setCapacity(null); setSafetyStatus(null);
const f = file || (fileRef.current?.files ? fileRef.current.files[0] : undefined);
if (!f) { setInfo('Please choose an image file (PNG recommended).'); return; }
setImageName(f.name);
const dataUrl = await new Promise<string>((res, rej) => {
const reader = new FileReader();
reader.onload = () => res(String(reader.result));
reader.onerror = rej;
reader.readAsDataURL(f);
});
const img = new Image();
img.src = dataUrl;
img.onload = () => {
const canvas = canvasRef.current!;
const ctx = canvas.getContext('2d')!;
canvas.width = img.width; canvas.height = img.height; ctx.drawImage(img, 0, 0);
setCapacity(computeCapacity(img.width, img.height));
const fnSafety = analyzeFilename(f.name);
if (fnSafety) {
setSafetyStatus(fnSafety);
setInfo(`Loaded ${img.width}x${img.height}. Warning: suspicious filename detected.`);
} else {
setInfo(`Loaded ${img.width}x${img.height} — capacity ${computeCapacity(img.width, img.height)} bytes`);
}
};
img.onerror = () => setInfo('Failed to load image.');
};
const handleEncode = () => {
setInfo(null);
if (safetyStatus && safetyStatus.status === 'Dangerous') return setInfo('Encoding blocked: image flagged DANGEROUS.');
const canvas = canvasRef.current!; const ctx = canvas.getContext('2d'); if (!ctx) return setInfo('Canvas not ready.');
const width = canvas.width; const height = canvas.height; if (!width || !height) return setInfo('Load an image first.');
const encoder = new TextEncoder(); const msgBytes = encoder.encode(message);
const max = computeCapacity(width, height);
if (msgBytes.length > max) return setInfo(`Message too long. Max ${max} bytes for this image.`);
const lengthBytes = new Uint8Array(4);
const len = msgBytes.length;
lengthBytes[0] = (len >> 24) & 0xff; lengthBytes[1] = (len >> 16) & 0xff; lengthBytes[2] = (len >> 8) & 0xff; lengthBytes[3] = len & 0xff;
const payload = new Uint8Array(4 + msgBytes.length); payload.set(lengthBytes, 0); payload.set(msgBytes, 4);
const bits = toBits(payload);
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
let bitIndex = 0;
for (let i = 0; i < data.length && bitIndex < bits.length; i += 4) {
data[i] = (data[i] & 0xfe) | bits[bitIndex++]; if (bitIndex >= bits.length) break;
data[i + 1] = (data[i + 1] & 0xfe) | bits[bitIndex++]; if (bitIndex >= bits.length) break;
data[i + 2] = (data[i + 2] & 0xfe) | bits[bitIndex++];
}
ctx.putImageData(imageData, 0, 0);
setInfo(`Message encoded successfully (${msgBytes.length} bytes). Use "Download Encoded Image" to save as PNG.`);
};
const handleDownload = () => {
if (safetyStatus && safetyStatus.status === 'Dangerous') return setInfo('Download blocked: image flagged DANGEROUS.');
const canvas = canvasRef.current!; if (!canvas) return setInfo('Nothing to download.');
const url = canvas.toDataURL('image/png'); const a = document.createElement('a'); a.href = url;
const base = imageName ? imageName.replace(/\.[^/.]+$/, '') : 'stego-image'; a.download = `${base}-encoded.png`; a.click();
};
return (
<div className="space-y-6">
<div className="flex items-center gap-3 mb-2">
<FileText className="w-6 h-6 text-primary" />
<h2 className="font-display text-2xl font-bold">Image Steganography Encode</h2>
</div>
<div className="p-4 bg-muted/30 rounded-lg">
<p className="text-sm text-muted-foreground">Upload a PNG image, enter a secret message, then Encode and Download the encoded PNG. The tool will flag suspicious filenames and block download if dangerous.</p>
</div>
<div className="flex gap-3 items-center">
<div className="flex items-center gap-2">
<input ref={fileRef} type="file" accept="image/*" onChange={() => handleImageLoad()} />
</div>
<div className="ml-auto text-sm text-muted-foreground">{imageName ?? 'No image loaded'}</div>
</div>
{capacity !== null && (<p className="text-sm text-muted-foreground">Capacity: <strong>{capacity}</strong> bytes (approx).</p>)}
<Textarea placeholder="Enter secret message to hide inside the image..." value={message} onChange={(e) => setMessage(e.target.value)} rows={6} />
<div className="flex gap-3">
<Button variant="neon" onClick={handleEncode} disabled={!message.trim()}>Encode</Button>
<Button variant="outline" onClick={handleDownload}>Download Encoded Image</Button>
</div>
{safetyStatus && (
<div className={cn("p-3 rounded-md mt-3 text-sm font-semibold",
safetyStatus.status === 'Dangerous' ? "bg-destructive/10 border border-destructive/30 text-destructive" : "bg-yellow-500/10 border border-yellow-500/30 text-yellow-300"
)}>
<div>Filename safety: <strong>{safetyStatus.status}</strong></div>
{safetyStatus.issues.length > 0 && <ul className="mt-2 text-sm">{safetyStatus.issues.map((i,idx)=>(<li key={idx}> {i}</li>))}</ul>}
{safetyStatus.status === 'Dangerous' && <div className="mt-2 font-semibold">DANGER: Encoding/download blocked.</div>}
</div>
)}
{info && <p className="text-sm text-muted-foreground">{info}</p>}
<canvas ref={canvasRef} className="hidden" />
</div>
);
};
/* ---------------- Updated StegoDecodeTool (detect links in LSB and QR payloads) ---------------- */
const StegoDecodeTool: React.FC = () => {
const fileRef = useRef<HTMLInputElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [imageName, setImageName] = useState<string | null>(null);
const [decoded, setDecoded] = useState<string | null>(null);
const [info, setInfo] = useState<string | null>(null);
const [qrPayload, setQrPayload] = useState<string | null>(null);
const [urlFindings, setUrlFindings] = useState<{ url: string; result: URLCheckResult }[]>([]);
const [overallUrlStatus, setOverallUrlStatus] = useState<URLCheckResult['status'] | null>(null);
const fromBitsToBytes = (bits: number[]) => {
const bytes: number[] = [];
for (let i = 0; i < bits.length; i += 8) {
let val = 0;
for (let j = 0; j < 8 && i + j < bits.length; j++) {
val = (val << 1) | bits[i + j];
}
bytes.push(val);
}
return new Uint8Array(bytes);
};
const tryLSBDecode = (ctx: CanvasRenderingContext2D, width: number, height: number): string | null => {
try {
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
const bits: number[] = [];
for (let i = 0; i < data.length; i += 4) { bits.push(data[i] & 1); bits.push(data[i + 1] & 1); bits.push(data[i + 2] & 1); }
if (bits.length < 32) return null;
const lenBits = bits.slice(0, 32);
let msgLen = 0;
for (let i = 0; i < 32; i++) msgLen = (msgLen << 1) | lenBits[i];
if (msgLen === 0) return '';
const totalBitsNeeded = 32 + msgLen * 8;
if (bits.length < totalBitsNeeded) return null;
const msgBits = bits.slice(32, 32 + msgLen * 8);
const bytes = fromBitsToBytes(msgBits);
try { return new TextDecoder().decode(bytes); } catch { return null; }
} catch (e) {
return null;
}
};
const handleImageLoad = async (file?: File) => {
setInfo(null); setDecoded(null); setQrPayload(null); setUrlFindings([]); setOverallUrlStatus(null);
const f = file || (fileRef.current?.files ? fileRef.current.files[0] : undefined);
if (!f) { setInfo('Please choose an image file.'); return; }
setImageName(f.name);
const dataUrl = await new Promise<string>((res, rej) => {
const reader = new FileReader(); reader.onload = () => res(String(reader.result)); reader.onerror = rej; reader.readAsDataURL(f);
});
const img = new Image(); img.src = dataUrl;
img.onload = () => {
const canvas = canvasRef.current!; const ctx = canvas.getContext('2d')!; canvas.width = img.width; canvas.height = img.height; ctx.drawImage(img, 0, 0);
setInfo(`Loaded ${img.width}x${img.height}. Ready to decode.`);
try {
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const code = jsQR(imageData.data, imageData.width, imageData.height, { inversionAttempts: 'attemptBoth' });
if (code && code.data) {
setQrPayload(code.data);
}
} catch (e) {}
};
img.onerror = () => setInfo('Failed to load image.');
};
const analyzeExtractedTexts = (texts: string[]) => {
const urls = texts.flatMap(t => extractUrlsFromText(t || ''));
const { overall, details } = analyzeUrls(urls);
setOverallUrlStatus(overall as URLCheckResult['status']);
setUrlFindings(details.map(d => ({ url: d.url, result: d.result })));
return { urls, overall };
};
const handleDecode = () => {
setInfo(null); setDecoded(null); setUrlFindings([]); setOverallUrlStatus(null);
const canvas = canvasRef.current!; const ctx = canvas.getContext('2d'); if (!ctx) return setInfo('Canvas not ready.');
const width = canvas.width; const height = canvas.height; if (!width || !height) return setInfo('Load an image first.');
const lsbText = tryLSBDecode(ctx, width, height);
if (lsbText === null) {
if (qrPayload) { setInfo('No LSB hidden message found. QR payload detected instead.'); analyzeExtractedTexts([qrPayload]); return; }
return setInfo('Image too small or no data found.');
}
if (lsbText === '') {
if (qrPayload) { setInfo('No hidden LSB message (length=0). QR payload found.'); analyzeExtractedTexts([qrPayload]); return; }
setDecoded(''); setInfo('No hidden message (length = 0).'); analyzeExtractedTexts([""]); return;
}
setDecoded(lsbText);
setInfo(`Decoded ${lsbText.length} characters from LSB.`);
analyzeExtractedTexts([lsbText, qrPayload ?? '']);
};
const handleCopy = async () => {
if (!decoded && !qrPayload) return;
if (overallUrlStatus === 'Dangerous') return setInfo('Copy blocked: image contains links flagged DANGEROUS.');
try {
await navigator.clipboard.writeText(decoded ?? qrPayload ?? '');
setInfo('Copied to clipboard.'); setTimeout(() => setInfo(null), 1200);
} catch {
setInfo('Copy failed.');
}
};
return (
<div className="space-y-6">
<div className="flex items-center gap-3 mb-2">
<Search className="w-6 h-6 text-primary" />
<h2 className="font-display text-2xl font-bold">Image Steganography Decode (Link Detector)</h2>
</div>
<div className="p-4 bg-muted/30 rounded-lg">
<p className="text-sm text-muted-foreground">Upload a PNG encoded with this tool. The tool extracts LSB hidden text and detects URLs (also reads QR payload). Any detected URLs are checked and flagged as Safe / Suspicious / Dangerous.</p>
</div>
<div className="flex gap-3 items-center">
<div className="flex items-center gap-2">
<input ref={fileRef} type="file" accept="image/*" onChange={() => handleImageLoad()} />
</div>
<div className="ml-auto text-sm text-muted-foreground">{imageName ?? 'No image loaded'}</div>
</div>
<div className="flex gap-3">
<Button variant="neon" onClick={handleDecode}>Decode</Button>
<Button variant="outline" onClick={handleCopy} disabled={!decoded && !qrPayload || overallUrlStatus === 'Dangerous'}>Copy Result</Button>
</div>
{overallUrlStatus && (
<div className={cn("p-4 rounded-md mt-3 text-sm",
overallUrlStatus === 'Dangerous' ? "bg-destructive/10 border border-destructive/30 text-destructive" :
overallUrlStatus === 'Suspicious' ? "bg-yellow-500/10 border border-yellow-500/30 text-yellow-300" : "bg-emerald-900/10 border border-emerald-500/20 text-emerald-300"
)}>
<div className="font-semibold">Detected link safety: {overallUrlStatus}</div>
{urlFindings.length > 0 && (
<div className="mt-2 text-sm">
<div className="text-xs text-muted-foreground">Found links and their analysis:</div>
<ul className="mt-2 text-sm space-y-1">
{urlFindings.map((f, idx) => (
<li key={idx} className="font-mono break-words">
<div className="flex items-start justify-between gap-3">
<div className="truncate">{f.url}</div>
<div className="text-sm ml-3">{f.result.status}</div>
</div>
{f.result.issues.length > 0 && (
<ul className="text-xs text-muted-foreground mt-1">
{f.result.issues.map((iss, i) => (<li key={i}> {iss}</li>))}
</ul>
)}
</li>
))}
</ul>
</div>
)}
{overallUrlStatus === 'Dangerous' && <div className="mt-2 font-semibold">DANGER: Image contains link(s) flagged as DANGEROUS. Do not follow links or download content from them.</div>}
</div>
)}
{decoded !== null && (
<div className="p-4 bg-muted/20 rounded-lg">
<p className="text-sm text-muted-foreground mb-2">Decoded message:</p>
<div className="font-mono text-sm break-words">{decoded.length ? decoded : <em>(empty)</em>}</div>
</div>
)}
{qrPayload && (
<div className="p-4 bg-muted/10 rounded border">
<p className="text-sm text-muted-foreground mb-2">Detected QR payload (image):</p>
<div className="font-mono text-sm break-words">{qrPayload}</div>
</div>
)}
{info && <p className="text-sm text-muted-foreground">{info}</p>}
<canvas ref={canvasRef} className="hidden" />
</div>
);
};
/* ---------------- QRScanner & BreachChecker (complete) ---------------- */
const QRScannerTool: React.FC = () => {
const videoRef = useRef<HTMLVideoElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const scanIntervalRef = useRef<number | null>(null);
const streamRef = useRef<MediaStream | null>(null);
const [scanning, setScanning] = useState(false);
const [cameraAvailable, setCameraAvailable] = useState<boolean | null>(null);
const [payload, setPayload] = useState<string | null>(null);
const [info, setInfo] = useState<string | null>(null);
const [urlCheckResult, setUrlCheckResult] = useState<URLCheckResult | null>(null);
const [paymentInfo, setPaymentInfo] = useState<{ isPayment: boolean; upiId?: string; name?: string; phone?: string } | null>(null);
const USE_BROWSER_ALERT = true;
useEffect(() => {
(async () => {
try {
const devices = await navigator.mediaDevices.enumerateDevices();
setCameraAvailable(devices.some(d => d.kind === 'videoinput'));
} catch (err) {
console.warn('enumerateDevices failed', err);
setCameraAvailable(false);
}
})();
return () => stopCamera();
}, []);
const setStatus = (s: string | null) => {
setInfo(s);
};
const emitAlert = (status: URLCheckResult['status'] | 'SafeText', message: string) => {
if (!USE_BROWSER_ALERT) return;
try {
if (status === 'Dangerous') {
window.alert(`DANGEROUS QR\n\n${message}`);
} else if (status === 'Suspicious') {
window.alert(`Suspicious QR\n\n${message}`);
} else if (status === 'Safe' || status === 'SafeText') {
window.alert(`Safe QR\n\n${message}`);
}
} catch (e) {
console.warn('alert blocked or failed', e);
}
};
const isLikelyUrl = (s: string) => {
try {
const u = new URL(s);
return !!u.protocol && (u.protocol.startsWith('http') || u.protocol.startsWith('https'));
} catch {
return false;
}
};
const sentenceForStatus = (status: URLCheckResult['status']) => {
if (status === 'Safe') return 'This QR code is Safe — no harmful or suspicious activity detected.';
if (status === 'Suspicious') return 'This QR code is Suspicious — it contains unusual patterns that may indicate a phishing attempt.';
return 'This QR code is Dangerous — it contains harmful or phishing-related content and should not be opened.';
};
const parsePaymentFromPayload = (p: string | null) => {
if (!p) return { isPayment: false } as const;
const s = p.trim();
const lower = s.toLowerCase();
let upiId: string | undefined;
let name: string | undefined;
let phone: string | undefined;
let isPayment = false;
try {
let urlForParams: URL | null = null;
if (s.includes('://')) {
try { urlForParams = new URL(s); } catch (e) { urlForParams = null; }
} else if (s.includes('?')) {
try { urlForParams = new URL('https://dummy?' + s.split('?')[1]); } catch (e) { urlForParams = null; }
}
if (urlForParams) {
const params = new URLSearchParams(urlForParams.search || urlForParams.hash?.substring(1) || '');
const pa = params.get('pa') || params.get('vpa') || params.get('upi') || params.get('address');
const pn = params.get('pn') || params.get('name');
const phoneParam = params.get('phone') || params.get('mobile') || params.get('msisdn');
if (pa) {
upiId = pa;
isPayment = true;
}
if (pn) name = pn;
if (phoneParam) phone = phoneParam;
}
} catch (e) {}
if (!upiId) {
const vpaMatch = s.match(/[a-z0-9.\-_]{2,}@[a-z0-9.\-]{2,}/i);
if (vpaMatch) {
upiId = vpaMatch[0];
isPayment = true;
}
}
if (!phone) {
const phoneParamMatch = s.match(/(?:phone|mobile|msisdn|tel|ph)=\+?([0-9\-\s\(\)]{6,20})/i);
if (phoneParamMatch) {
phone = phoneParamMatch[1].replace(/\D/g, '');
} else {
const digitMatch = s.match(/(\+91|91|0)?([6-9][0-9]{9})/);
if (digitMatch) {
const prefix = digitMatch[1] || '';
const core = digitMatch[2];
phone = (prefix || '') + core;
} else {
const any10 = s.match(/([0-9]{10,12})/);
if (any10) phone = any10[1];
}
}
}
if (!isPayment && (lower.startsWith('upi:') || lower.includes('upi://') || lower.includes('upi=') || lower.includes('paytm') || lower.includes('googlepay') || lower.includes('gpay'))) {
isPayment = true;
}
return { isPayment, upiId, name, phone };
};
const startCamera = async () => {
setPayload(null);
setUrlCheckResult(null);
setPaymentInfo(null);
setStatus('Requesting camera access...');
try {
let stream: MediaStream | null = null;
try {
stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: 'environment' } } });
} catch (err) {
console.warn('facingMode failed, trying generic camera', err);
stream = await navigator.mediaDevices.getUserMedia({ video: true });
}
streamRef.current = stream!;
if (videoRef.current) {
const v = videoRef.current;
v.srcObject = stream;
v.muted = true;
v.playsInline = true;
v.autoplay = true;
await v.play();
}
setScanning(true);
setStatus('Camera started — scanning...');
if (scanIntervalRef.current) {
window.clearInterval(scanIntervalRef.current);
scanIntervalRef.current = null;
}
scanIntervalRef.current = window.setInterval(runScan, 200);
} catch (err: any) {
console.error('[QRScanner] startCamera error', err);
if (err && err.name === 'NotAllowedError') setStatus('Camera permission denied. Allow camera to scan.');
else setStatus('Camera not available or permission blocked. Try image upload.');
setScanning(false);
}
};
const stopCamera = () => {
setScanning(false);
if (scanIntervalRef.current) {
window.clearInterval(scanIntervalRef.current);
scanIntervalRef.current = null;
}
const stream = streamRef.current;
if (stream) {
stream.getTracks().forEach(t => t.stop());
streamRef.current = null;
}
if (videoRef.current) {
try { videoRef.current.pause(); } catch {}
// @ts-ignore
videoRef.current.srcObject = null;
}
setStatus('Camera stopped.');
};
const runScan = () => {
try {
const video = videoRef.current;
const canvas = canvasRef.current;
if (!video || !canvas) return;
if (video.readyState !== HTMLMediaElement.HAVE_ENOUGH_DATA) return;
if (canvas.width !== video.videoWidth || canvas.height !== video.videoHeight) {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
}
const ctx = canvas.getContext('2d');
if (!ctx) return;
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const code = jsQR(imageData.data, imageData.width, imageData.height, { inversionAttempts: 'attemptBoth' });
if (code && code.data) {
setPayload(code.data);
setStatus('QR decoded from camera.');
stopCamera();
const payment = parsePaymentFromPayload(code.data);
setPaymentInfo(payment.isPayment ? payment : null);
if (isLikelyUrl(code.data)) {
const res = checkUrlString(code.data);
setUrlCheckResult(res);
const issuesMsg = res.issues.length ? res.issues.map(i => `${i}`).join('\n') : 'No obvious risks detected.';
if (res.status === 'Dangerous') {
setStatus('⚠️ Dangerous QR detected — do not open the link.');
emitAlert(res.status, `This QR contains a URL flagged as DANGEROUS.\n\nIssues:\n${issuesMsg}`);
} else if (res.status === 'Suspicious') {
setStatus('⚠️ Suspicious QR detected — proceed with caution.');
emitAlert(res.status, `This QR contains a URL flagged as SUSPICIOUS.\n\nIssues:\n${issuesMsg}`);
} else {
setStatus('✅ Safe QR detected.');
emitAlert(res.status, 'This QR contains a URL and no obvious risks were found.');
}
} else {
setUrlCheckResult(null);
if (payment.isPayment) {
emitAlert('SafeText', 'Payment QR decoded — extracted payment fields.');
setStatus('✅ Payment QR decoded — extracted fields below.');
} else {
emitAlert('SafeText', 'QR decoded (non-URL payload).');
}
}
}
} catch (err) {
console.error('[QRScanner] runScan error', err);
}
};
const handleImageUpload = async (file?: File) => {
setPayload(null);
setUrlCheckResult(null);
setPaymentInfo(null);
setStatus('Processing image...');
try {
const f = file ?? (document.getElementById('qr-file') as HTMLInputElement)?.files?.[0];
if (!f) return setStatus('Choose an image file with a QR code.');
const dataUrl = await new Promise<string>((res, rej) => {
const r = new FileReader();
r.onload = () => res(String(r.result));
r.onerror = rej;
r.readAsDataURL(f);
});
const img = new Image();
img.src = dataUrl;
img.onload = () => {
const canvas = canvasRef.current!;
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d')!;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const result = jsQR(imageData.data, imageData.width, imageData.height, { inversionAttempts: 'attemptBoth' });
if (result && result.data) {
setPayload(result.data);
setStatus('QR decoded from image.');
const payment = parsePaymentFromPayload(result.data);
setPaymentInfo(payment.isPayment ? payment : null);
if (isLikelyUrl(result.data)) {
const res = checkUrlString(result.data);
setUrlCheckResult(res);
const issuesMsg = res.issues.length ? res.issues.map(i => `${i}`).join('\n') : 'No obvious risks detected.';
if (res.status === 'Dangerous') {
setStatus('⚠️ Dangerous QR detected — do not open the link.');
emitAlert(res.status, `This QR contains a URL flagged as DANGEROUS.\n\nIssues:\n${issuesMsg}`);
} else if (res.status === 'Suspicious') {
setStatus('⚠️ Suspicious QR detected — proceed with caution.');
emitAlert(res.status, `This QR contains a URL flagged as SUSPICIOUS.\n\nIssues:\n${issuesMsg}`);
} else {
setStatus('✅ Safe QR detected.');
emitAlert(res.status, 'This QR contains a URL and no obvious risks were found.');
}
} else {
if (payment.isPayment) {
emitAlert('SafeText', 'Payment QR decoded — extracted payment fields.');
setStatus('✅ Payment QR decoded — extracted fields below.');
} else {
setUrlCheckResult(null);
emitAlert('SafeText', 'QR decoded (non-URL payload).');
}
}
} else {
setStatus('No QR code found in image.');
}
};
img.onerror = () => {
console.error('[QRScanner] image load failed');
setStatus('Failed to load image.');
};
} catch (err) {
console.error('[QRScanner] handleImageUpload error', err);
setStatus('Image processing failed.');
}
};
const handleManual = (txt: string) => {
setPayload(txt || null);
setUrlCheckResult(null);
setPaymentInfo(null);
if (txt) {
const payment = parsePaymentFromPayload(txt);
setPaymentInfo(payment.isPayment ? payment : null);
if (isLikelyUrl(txt)) {
const res = checkUrlString(txt);
setUrlCheckResult(res);
const issuesMsg = res.issues.length ? res.issues.map(i => `${i}`).join('\n') : 'No obvious risks detected.';
if (res.status === 'Dangerous') {
setStatus('⚠️ Dangerous URL (manual input).');
emitAlert(res.status, `Manual URL flagged DANGEROUS.\n\nIssues:\n${issuesMsg}`);
} else if (res.status === 'Suspicious') {
setStatus('⚠️ Suspicious URL (manual input).');
emitAlert(res.status, `Manual URL flagged SUSPICIOUS.\n\nIssues:\n${issuesMsg}`);
} else {
setStatus('✅ Safe URL (manual input).');
emitAlert(res.status, 'Manual input looks safe.');
}
} else if (payment.isPayment) {
setStatus('✅ Payment string detected (manual input).');
emitAlert('SafeText', 'Manual input looks like a payment/UPI payload.');
}
}
};
const singleSentenceOutput = (() => {
if (!payload) return null;
if (isLikelyUrl(payload) && urlCheckResult) {
return sentenceForStatus(urlCheckResult.status);
}
return 'This QR code is Safe — no harmful or suspicious activity detected.';
})();
return (
<div className="space-y-6">
<div className="flex items-center gap-3 mb-2">
<Camera className="w-6 h-6 text-primary" />
<h2 className="font-display text-2xl font-bold">QR Code Security Scanner</h2>
</div>
<div className="p-4 bg-muted/30 rounded-lg">
<p className="text-sm text-muted-foreground">
<Info className="w-4 h-4 inline mr-1" />
Scan QR via camera or upload an image. If QR contains a URL, the tool will automatically check it for risks.
If it looks like a payment/UPI QR, the tool will attempt to extract UPI ID and phone number.
</p>
</div>
<div className="flex gap-3 items-center">
<Button variant="neon" onClick={() => (scanning ? stopCamera() : startCamera())}>
{scanning ? 'Stop Camera' : 'Start Camera'}
</Button>
<input id="qr-file" type="file" accept="image/*" onChange={() => handleImageUpload()} />
<div className="ml-auto text-sm text-muted-foreground">
{cameraAvailable === null ? 'Checking camera...' : cameraAvailable ? 'Camera available' : 'No camera'}
</div>
</div>
<video ref={videoRef} className={cn(scanning ? 'block w-full rounded-lg' : 'hidden')} style={{ maxHeight: 360 }} playsInline muted autoPlay />
<canvas ref={canvasRef} className="hidden" />
<div>
<p className="text-sm text-muted-foreground mb-2">Decoded payload:</p>
<Textarea
placeholder="Decoded QR payload shows here..."
value={payload ?? ''}
onChange={(e) => handleManual(e.target.value)}
rows={4}
/>
</div>
{payload && (
<div className="p-4 rounded-lg space-y-3" style={{ border: '1px solid rgba(255,255,255,0.03)' }}>
<div className="flex items-start justify-between gap-3">
<div className="font-mono break-all text-sm">{payload}</div>
<div className="flex items-center gap-2">
<Button variant="outline" onClick={() => navigator.clipboard.writeText(payload)}>Copy</Button>
{isLikelyUrl(payload) ? (
<>
<Button variant="neon" onClick={() => {
const res = checkUrlString(payload);
setUrlCheckResult(res);
const issuesMsg = res.issues.length ? res.issues.map(i => `${i}`).join('\n') : 'No obvious risks detected.';
if (res.status === 'Dangerous') {
setStatus('⚠️ Dangerous QR detected — do not open the link.');
emitAlert(res.status, `This QR contains a URL flagged as DANGEROUS.\n\nIssues:\n${issuesMsg}`);
} else if (res.status === 'Suspicious') {
setStatus('⚠️ Suspicious QR detected — proceed with caution.');
emitAlert(res.status, `This QR contains a URL flagged as SUSPICIOUS.\n\nIssues:\n${issuesMsg}`);
} else {
setStatus('✅ Safe QR detected.');
emitAlert(res.status, 'This QR contains a URL and no obvious risks were found.');
}
}}>Run URL Check</Button>
<a
href={payload}
target="_blank"
rel="noreferrer"
className={cn("underline px-3 py-1 rounded", urlCheckResult?.status === 'Dangerous' ? 'text-destructive' : '')}
onClick={(e) => {
if (urlCheckResult?.status === 'Dangerous') {
if (!confirm('Warning: this link is flagged DANGEROUS. Are you sure you want to open it?')) e.preventDefault();
}
}}
>
Open
</a>
</>
) : null}
</div>
</div>
{singleSentenceOutput && (
<div
className={cn(
"p-3 rounded-md mt-2 text-sm font-semibold",
(urlCheckResult && urlCheckResult.status !== 'Safe') ?
"bg-red-900/20 border border-red-500/30 text-red-300 shadow-[0_0_12px_rgba(239,68,68,0.12)]" :
"bg-emerald-900/20 border border-emerald-500/30 text-emerald-300 shadow-[0_0_12px_rgba(16,185,129,0.10)]"
)}
>
{singleSentenceOutput}
</div>
)}
{paymentInfo && paymentInfo.isPayment && (
<div className="mt-2 p-3 bg-muted/10 rounded-md">
<div className="flex items-center gap-2 mb-2">
<CheckCircle className="w-5 h-5 text-green-400" />
<span className="font-display font-semibold">Payment fields detected</span>
</div>
<div className="space-y-2 text-sm">
{paymentInfo.upiId && (
<div className="flex items-center justify-between gap-3">
<div className="truncate">
<div className="text-xs text-muted-foreground">UPI ID</div>
<div className="font-mono">{paymentInfo.upiId}</div>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={() => navigator.clipboard.writeText(paymentInfo.upiId)}>Copy</Button>
</div>
</div>
)}
{paymentInfo.name && (
<div className="flex items-center justify-between gap-3">
<div>
<div className="text-xs text-muted-foreground">Payee Name</div>
<div className="truncate">{paymentInfo.name}</div>
</div>
<div>
<Button variant="outline" onClick={() => paymentInfo.name && navigator.clipboard.writeText(paymentInfo.name)}>Copy</Button>
</div>
</div>
)}
{paymentInfo.phone && (
<div className="flex items-center justify-between gap-3">
<div>
<div className="text-xs text-muted-foreground">Phone</div>
<div className="font-mono">{paymentInfo.phone}</div>
</div>
<div>
<Button variant="outline" onClick={() => paymentInfo.phone && navigator.clipboard.writeText(paymentInfo.phone)}>Copy</Button>
</div>
</div>
)}
{!paymentInfo.upiId && !paymentInfo.phone && (
<div className="text-sm text-muted-foreground">Detected as payment-like payload but no UPI ID or phone could be extracted.</div>
)}
</div>
</div>
)}
{urlCheckResult && (
<div className="mt-2">
<div className="flex items-center gap-2 mb-2">
{urlCheckResult.status === 'Safe' && <CheckCircle className="w-5 h-5 text-green-400" />}
{urlCheckResult.status === 'Suspicious' && <AlertTriangle className="w-5 h-5 text-yellow-500" />}
{urlCheckResult.status === 'Dangerous' && <XCircle className="w-5 h-5 text-destructive" />}
<span className="font-display font-semibold">{urlCheckResult.status}</span>
</div>
{urlCheckResult.issues.length > 0 ? (
<ul className="text-sm space-y-1">
{urlCheckResult.issues.map((iss, i) => (
<li key={i} className="flex items-start gap-2">
<AlertTriangle className="w-4 h-4 mt-0.5 text-yellow-500" />
<span className="text-muted-foreground">{iss}</span>
</li>
))}
</ul>
) : (
<p className="text-sm text-muted-foreground">No obvious risks detected.</p>
)}
</div>
)}
</div>
)}
{info && <p className="text-sm text-muted-foreground">{info}</p>}
</div>
);
};
const normalizeGmail = (email: string) => {
const e = email.trim().toLowerCase();
if (!e.includes('@')) return e;
const [local, domain] = e.split('@');
if (!domain.includes('gmail.com') && !domain.includes('googlemail.com')) {
return e;
}
const localNoPlus = local.split('+')[0].replace(/\./g, '');
return `${localNoPlus}@gmail.com`;
};
const normalizePhone = (p: string) => {
if (!p) return '';
const digits = p.replace(/[^\d]/g, '');
if (digits.length <= 10) return digits;
return digits.slice(-10);
};
const isGmailAddress = (s: string) => /\b[A-Za-z0-9._%+-]+@gmail\.com\b/i.test(s);
export const VECTOR_LABELS: Record<string, string> = {
'third-party': 'Third-party vendor compromise',
'misconfigured-storage': 'Misconfigured storage / public backup',
'scraped': 'Scraped from public profiles / forums',
'paste-site': 'Posted on paste/forum site',
'credential-stuffing': 'Credential stuffing / password reuse',
'insider': 'Insider leak / accidental exposure',
'unknown': 'Unknown / not specified'
};
const simulatedBreaches = [
{
id: 'breach-1',
title: 'Example Social Site Breach',
date: '2023-05-10',
fields: ['email', 'password', 'name'],
sampleMatches: ['alice@example.com', 'bob@gmail.com', '+919876543210'],
vector: 'third-party',
notes: 'Demo breach from a third-party vendor integration',
confidence: 'high',
},
{
id: 'breach-2',
title: 'Forum Dump 2022',
date: '2022-11-01',
fields: ['email', 'phone'],
sampleMatches: ['charlie@gmail.com', '9876543210'],
vector: 'paste-site',
notes: 'Public paste site leak example',
confidence: 'medium',
},
];
const BreachCheckerLocal: React.FC = () => {
const [identifier, setIdentifier] = useState('');
const [consent, setConsent] = useState(false);
const [results, setResults] = useState<Array<{
breachId: string; breachTitle: string; breachDate: string; leakedFields: string[]; reason: string; vector?: string; notes?: string; confidence?: string; for?: string;
}> | null>(null);
const [info, setInfo] = useState<string | null>(null);
const breaches = useMemo(() => simulatedBreaches, []);
const runSingleCheck = () => {
setInfo(null);
setResults(null);
if (!identifier.trim()) return setInfo('Please enter an email or phone number.');
if (!consent) return setInfo('Please give consent before checking.');
const id = identifier.trim();
const matches: any[] = [];
const isEmail = id.includes('@');
const normalizedEmail = isEmail ? id.toLowerCase() : '';
const normalizedGmail = isEmail && isGmailAddress(id) ? normalizeGmail(id) : null;
const normalizedPhone = !isEmail ? normalizePhone(id) : null;
for (const b of breaches) {
for (const sample of b.sampleMatches) {
const sRaw = String(sample);
const s = sRaw.toLowerCase();
if (isEmail && s === normalizedEmail) {
matches.push({ breachId: b.id, breachTitle: b.title, breachDate: b.date, leakedFields: b.fields, reason: 'Exact match', vector: b.vector, notes: b.notes, confidence: b.confidence });
break;
}
if (!isEmail && normalizedPhone && s.replace(/[^\d]/g, '').endsWith(normalizedPhone)) {
matches.push({ breachId: b.id, breachTitle: b.title, breachDate: b.date, leakedFields: b.fields, reason: 'Phone suffix match', vector: b.vector, notes: b.notes, confidence: b.confidence });
break;
}
if (normalizedGmail) {
const sampleNorm = isGmailAddress(s) ? normalizeGmail(s) : s;
if (sampleNorm === normalizedGmail) {
matches.push({ breachId: b.id, breachTitle: b.title, breachDate: b.date, leakedFields: b.fields, reason: 'Gmail-normalized match', vector: b.vector, notes: b.notes, confidence: b.confidence });
break;
}
}
if (isEmail && normalizedEmail.includes('@')) {
const enteredDomain = normalizedEmail.split('@')[1];
if (s.includes('@' + enteredDomain)) {
matches.push({ breachId: b.id, breachTitle: b.title, breachDate: b.date, leakedFields: b.fields, reason: `Domain match (${enteredDomain})`, vector: b.vector, notes: b.notes, confidence: b.confidence });
break;
}
}
}
}
const priority: Record<string, number> = { 'Exact match': 4, 'Gmail-normalized match': 3, 'Phone suffix match': 3, 'Domain match': 1 };
const dedup = new Map<string, any>();
for (const m of matches) {
const cur = dedup.get(m.breachId);
const reasonKey = m.reason.startsWith('Domain match') ? 'Domain match' : m.reason;
const curReasonKey = cur ? (cur.reason.startsWith('Domain match') ? 'Domain match' : cur.reason) : null;
if (!cur || (priority[reasonKey] || 0) > (priority[curReasonKey] || 0)) dedup.set(m.breachId, m);
}
const final = Array.from(dedup.values());
setResults(final.length ? final : []);
if (final.length === 0) setInfo('No breaches found in local dataset (demo only).');
};
const handleCsvUpload = async (file?: File) => {
setInfo(null);
setResults(null);
if (!file) return setInfo('Choose a CSV file (one email/phone per line).');
if (!consent) return setInfo('Please give consent before bulk checking.');
const text = await file.text();
const lines = text.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
const aggregate: any[] = [];
for (const line of lines) {
const id = line;
const isEmail = id.includes('@');
const normEmail = isEmail ? id.toLowerCase() : '';
const normGmail = isEmail && isGmailAddress(id) ? normalizeGmail(id) : null;
const normPhone = !isEmail ? normalizePhone(id) : null;
const localMatches: any[] = [];
for (const b of breaches) {
for (const sample of b.sampleMatches) {
const sRaw = String(sample);
const s = sRaw.toLowerCase();
if (isEmail && s === normEmail) {
localMatches.push({ breachId: b.id, breachTitle: b.title, breachDate: b.date, leakedFields: b.fields, reason: 'Exact match', vector: b.vector, notes: b.notes, confidence: b.confidence });
break;
}
if (!isEmail && normPhone && s.replace(/[^\d]/g, '').endsWith(normPhone)) {
localMatches.push({ breachId: b.id, breachTitle: b.title, breachDate: b.date, leakedFields: b.fields, reason: 'Phone suffix match', vector: b.vector, notes: b.notes, confidence: b.confidence });
break;
}
if (normGmail) {
const sampleNorm = isGmailAddress(s) ? normalizeGmail(s) : s;
if (sampleNorm === normGmail) {
localMatches.push({ breachId: b.id, breachTitle: b.title, breachDate: b.date, leakedFields: b.fields, reason: 'Gmail-normalized match', vector: b.vector, notes: b.notes, confidence: b.confidence });
break;
}
}
if (isEmail && normEmail.includes('@')) {
const enteredDomain = normEmail.split('@')[1];
if (s.includes('@' + enteredDomain)) {
localMatches.push({ breachId: b.id, breachTitle: b.title, breachDate: b.date, leakedFields: b.fields, reason: `Domain match (${enteredDomain})`, vector: b.vector, notes: b.notes, confidence: b.confidence });
break;
}
}
}
}
const ded = new Map<string, any>();
const priority: Record<string, number> = { 'Exact match': 4, 'Gmail-normalized match': 3, 'Phone suffix match': 3, 'Domain match': 1 };
for (const m of localMatches) {
const key = m.reason.startsWith('Domain match') ? 'Domain match' : m.reason;
const cur = ded.get(m.breachId);
const curKey = cur ? (cur.reason.startsWith('Domain match') ? 'Domain match' : cur.reason) : null;
if (!cur || (priority[key] || 0) > (priority[curKey] || 0)) ded.set(m.breachId, m);
}
for (const m of Array.from(ded.values())) {
aggregate.push({ ...m, for: id });
}
}
setResults(aggregate.length ? aggregate : []);
if (aggregate.length === 0) setInfo('No matches for any entries in uploaded CSV (demo dataset).');
};
const exportJSON = () => {
const payload = { checkedAt: new Date().toISOString(), identifier, results };
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `breach-check-${(identifier || 'bulk').replace(/[^a-z0-9]/gi,'_')}.json`;
a.click();
URL.revokeObjectURL(url);
};
const exportCSV = () => {
if (!results || results.length === 0) return;
const lines = ['breachId,breachTitle,breachDate,leakedFields,reason,for'];
for (const r of results) {
lines.push([r.breachId, r.breachTitle, r.breachDate, (r.leakedFields || []).join('|'), r.reason, r.for || identifier].map(v => `"${String(v).replace(/"/g,'""')}"`).join(','));
}
const blob = new Blob([lines.join('\n')], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `breach-results-${(identifier || 'bulk').replace(/[^a-z0-9]/gi,'_')}.csv`;
a.click();
URL.revokeObjectURL(url);
};
return (
<div className="space-y-6">
<div className="flex items-center gap-3">
<AlertTriangle className="w-6 h-6 text-accent" />
<h2 className="font-display text-2xl font-bold">Local Breach Checker (Gmail & Phone)</h2>
</div>
<p className="text-sm text-muted-foreground">
This tool runs <strong>locally</strong> and uses a simulated breach dataset. No external API is called.
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 items-center">
<Input placeholder="Enter Gmail or phone (e.g., alice@gmail.com or +919876543210)" value={identifier} onChange={(e) => setIdentifier(e.target.value)} />
<label className="flex items-center gap-2 text-sm">
<input type="checkbox" checked={consent} onChange={(e) => setConsent(e.target.checked)} />
<span>I consent to local checking (no data leaves this device)</span>
</label>
<div className="flex gap-2">
<Button variant="neon" onClick={runSingleCheck}>Check</Button>
<label className="cursor-pointer inline-flex items-center px-3 py-2 border rounded bg-muted/10">
<FileText className="w-4 h-4 mr-2" />
<input type="file" accept=".csv" style={{ display: 'none' }} onChange={(e) => e.target.files && handleCsvUpload(e.target.files[0])} />
Bulk CSV
</label>
</div>
</div>
{info && <p className="text-sm text-muted-foreground">{info}</p>}
{results && results.length > 0 && (
<div className="space-y-4">
{results.map((r, idx) => (
<div key={idx} className="p-4 bg-muted/20 rounded border">
<div className="flex items-start justify-between">
<div>
<h4 className="font-semibold">{r.breachTitle} <span className="text-sm text-muted-foreground">({r.breachDate})</span></h4>
<p className="text-sm text-muted-foreground mt-1">Leaked fields: {r.leakedFields.join(', ')}</p>
<p className="text-sm text-muted-foreground mt-1"><strong>How data breach (inferred):</strong> {VECTOR_LABELS[r.vector || 'unknown']}</p>
{r.notes && <p className="text-sm text-muted-foreground mt-1"><em>{r.notes}</em></p>}
</div>
<div className="text-right">
<div className="text-xs text-muted-foreground">Matched because</div>
<div className="font-mono text-sm">{r.reason}{r.for ? ` (for ${r.for})` : ''}</div>
<div className="text-xs text-muted-foreground mt-2">Confidence</div>
<div className="font-semibold">{r.confidence || 'medium'}</div>
</div>
</div>
</div>
))}
<div className="flex gap-2">
<Button variant="outline" onClick={() => navigator.clipboard.writeText(JSON.stringify(results, null, 2))}>Copy Results</Button>
<Button variant="neon" onClick={exportJSON}><Download className="w-4 h-4 mr-2 inline" />Download JSON</Button>
<Button variant="outline" onClick={exportCSV}>Download CSV</Button>
</div>
</div>
)}
</div>
);
};
export default ToolsDashboard;