1721 lines
71 KiB
TypeScript
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;
|