From 458b7e9359ff37106e00df42d7919e6b0b0dfc08 Mon Sep 17 00:00:00 2001 From: GOKUL-S-2006 Date: Wed, 3 Jun 2026 16:38:12 +0530 Subject: [PATCH] feat: add IP/Subnet Calculator to Standalone Tools (closes #45) --- frontend/src/components/tools/ToolPanels.tsx | 152 +++++++++++++++++++ frontend/src/components/views/IdleView.tsx | 3 +- frontend/src/lib/types.ts | 2 +- frontend/src/messages/de.json | 3 +- frontend/src/messages/en.json | 3 +- frontend/src/messages/es.json | 3 +- frontend/src/messages/fr.json | 3 +- frontend/src/messages/ru.json | 3 +- 8 files changed, 165 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/tools/ToolPanels.tsx b/frontend/src/components/tools/ToolPanels.tsx index ffd871c..56b4337 100644 --- a/frontend/src/components/tools/ToolPanels.tsx +++ b/frontend/src/components/tools/ToolPanels.tsx @@ -365,10 +365,161 @@ function HeadersPanel() { ); } +function SubnetPanel() { + const { t } = useTranslations(); + + // ── pure-JS subnet math (no external deps) ────────────────────────────── + function ipToInt(ip: string): number { + return ip.split('.').reduce((acc, oct) => (acc << 8) + parseInt(oct, 10), 0) >>> 0; + } + function intToIp(n: number): string { + return [(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff].join('.'); + } + function cidrToMask(cidr: number): number { + return cidr === 0 ? 0 : (0xffffffff << (32 - cidr)) >>> 0; + } + function maskToCidr(mask: number): number { + let n = mask >>> 0, count = 0; + while (n & 0x80000000) { count++; n = (n << 1) >>> 0; } + return count; + } + function isValidIp(ip: string): boolean { + const parts = ip.split('.'); + if (parts.length !== 4) return false; + return parts.every(p => { const n = parseInt(p, 10); return !isNaN(n) && n >= 0 && n <= 255 && String(n) === p; }); + } + function isValidMask(mask: string): boolean { + if (!isValidIp(mask)) return false; + const inv = (~ipToInt(mask)) >>> 0; + return (inv & (inv + 1)) === 0; + } + function getIpType(ip: string): string { + const n = ipToInt(ip); + if ((n >>> 24) === 10 || ((n >>> 16) & 0xfff0) === 0xac10 || (n >>> 16) === 0xc0a8) return 'Private (RFC 1918)'; + if ((n >>> 24) === 127) return 'Loopback'; + if ((n >>> 28) === 0xe) return 'Multicast'; + return 'Public'; + } + + const [ip, setIp] = useState(''); + const [prefix, setPrefix] = useState('24'); + const [prefixMode, setPrefixMode] = useState<'cidr' | 'mask'>('cidr'); + const [errors, setErrors] = useState<{ ip?: string; prefix?: string }>({}); + const [result, setResult] = useState(null); + + const run = () => { + const errs: { ip?: string; prefix?: string } = {}; + if (!ip.trim()) errs.ip = 'IP address is required'; + else if (!isValidIp(ip.trim())) errs.ip = 'Invalid IP (e.g. 192.168.1.0)'; + + let cidr = 0; + if (prefixMode === 'cidr') { + const n = parseInt(prefix.replace('/', ''), 10); + if (isNaN(n) || n < 0 || n > 32) errs.prefix = 'CIDR must be 0–32'; + else cidr = n; + } else { + if (!isValidMask(prefix.trim())) errs.prefix = 'Invalid subnet mask'; + else cidr = maskToCidr(ipToInt(prefix.trim())); + } + + setErrors(errs); + if (Object.keys(errs).length > 0) return; + + const mask = cidrToMask(cidr); + const ipInt = ipToInt(ip.trim()); + const netInt = (ipInt & mask) >>> 0; + const bcastInt = (netInt | (~mask >>> 0)) >>> 0; + const total = Math.pow(2, 32 - cidr); + const usable = cidr >= 31 ? total : Math.max(0, total - 2); + + setResult({ + cidr, + networkAddress: intToIp(netInt), + broadcastAddress: intToIp(bcastInt), + subnetMask: intToIp(mask), + wildcardMask: intToIp(~mask >>> 0), + firstUsable: cidr >= 31 ? intToIp(netInt) : intToIp(netInt + 1), + lastUsable: cidr >= 31 ? intToIp(bcastInt) : intToIp(bcastInt - 1), + usableHosts: usable, + totalHosts: total, + ipType: getIpType(ip.trim()), + }); + }; + + return ( +
+ + {/* Mode toggle */} +
+ {(['cidr', 'mask'] as const).map(m => ( + + ))} +
+ {/* Inputs */} +
+
+ { setIp(e.target.value); setErrors(p => ({ ...p, ip: undefined })); }} + placeholder="192.168.1.0" onKeyDown={e => e.key === 'Enter' && run()} /> + {errors.ip && {errors.ip}} +
+
+ {prefixMode === 'cidr' ? ( + { setPrefix(e.target.value); setErrors(p => ({ ...p, prefix: undefined })); }} + placeholder="/24" onKeyDown={e => e.key === 'Enter' && run()} /> + ) : ( + { setPrefix(e.target.value); setErrors(p => ({ ...p, prefix: undefined })); }} + placeholder="255.255.255.0" onKeyDown={e => e.key === 'Enter' && run()} /> + )} + {errors.prefix && {errors.prefix}} +
+ +
+ + {/* CIDR slider */} + {prefixMode === 'cidr' && ( +
+ setPrefix(e.target.value)} + className="w-full accent-blue cursor-pointer" /> +
+ {[0, 8, 16, 24, 32].map(n => ( + + ))} +
+
+ )} +
+ + {result && ( + + + + + + + + + + + + )} +
+ ); +} const TOOL_TITLES: Record = { crypto: 'Crypto Address Lookup', qr: 'QR Code Decoder', metadata: 'File Metadata & GEOINT', headers: 'Email Header Analyzer', mac: 'MAC Lookup', + subnet: 'IP / Subnet Calculator', }; interface Props { @@ -395,6 +546,7 @@ export function ToolPanels({ mode, onBack }: Props) { {activePanel === 'qr' && } {activePanel === 'metadata' && } {activePanel === 'headers' && } + {activePanel === 'subnet' && } {activePanel === 'mac' && } ); diff --git a/frontend/src/components/views/IdleView.tsx b/frontend/src/components/views/IdleView.tsx index 6cf284b..88ed42f 100644 --- a/frontend/src/components/views/IdleView.tsx +++ b/frontend/src/components/views/IdleView.tsx @@ -5,10 +5,11 @@ import { useTranslations } from '@/lib/i18n'; import { Logo } from '../Logo'; import type { ToolMode } from '@/lib/types'; -const TOOL_IDS = ['metadata', 'headers', 'crypto', 'qr', 'mac'] as const; +const TOOL_IDS = ['metadata', 'headers', 'crypto', 'qr', 'mac', 'subnet'] as const; type ToolId = typeof TOOL_IDS[number]; const ICONS: Record = { + subnet: Globe, metadata: FileText, headers: Mail, crypto: Bitcoin, diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 7b5928d..9ead9b3 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -1,6 +1,6 @@ export type ScanType = 'domain' | 'ip' | 'email' | 'phone' | 'username'; export type ScanStatus = 'idle' | 'running' | 'completed' | 'failed'; -export type ToolMode = 'metadata' | 'headers' | 'crypto' | 'qr' | 'mac' | null; +export type ToolMode = 'metadata' | 'headers' | 'crypto' | 'qr' | 'mac' | 'subnet' | null; export interface ScanMeta { id: string; diff --git a/frontend/src/messages/de.json b/frontend/src/messages/de.json index 9019329..2b24eed 100644 --- a/frontend/src/messages/de.json +++ b/frontend/src/messages/de.json @@ -75,7 +75,8 @@ "headers": { "label": "E-Mail-Header", "desc": "SPF, DKIM, Routing-Hops" }, "crypto": { "label": "Krypto-Adresse", "desc": "Bitcoin und Ethereum" }, "qr": { "label": "QR-Decoder", "desc": "Decodieren & analysieren" }, - "mac": { "label": "MAC Lookup", "desc": "Hersteller-Suche" } + "mac": { "label": "MAC Lookup", "desc": "Hersteller-Suche" }, + "subnet": { "label": "IP / Subnetz", "desc": "Netzwerkbereiche & CIDR" } }, "demo": { "line1": "Dies ist eine öffentliche Demo mit begrenzten API-Kontingenten. Manche Module (Shodan, VirusTotal) können ratenbegrenzt sein.", diff --git a/frontend/src/messages/en.json b/frontend/src/messages/en.json index 77751ca..0940d3d 100644 --- a/frontend/src/messages/en.json +++ b/frontend/src/messages/en.json @@ -75,7 +75,8 @@ "headers": { "label": "Email Headers", "desc": "SPF, DKIM, routing hops" }, "crypto": { "label": "Crypto Address", "desc": "Bitcoin & Ethereum" }, "qr": { "label": "QR Decode", "desc": "Decode & analyze" }, - "mac": { "label": "MAC Lookup", "desc": "Vendor lookup" } + "mac": { "label": "MAC Lookup", "desc": "Vendor lookup" }, + "subnet": { "label": "IP / Subnet", "desc": "Network ranges & CIDR" } }, "demo": { "line1": "This is a public demo with limited API quotas. Some modules (Shodan, VirusTotal) may be rate-limited.", diff --git a/frontend/src/messages/es.json b/frontend/src/messages/es.json index 2485e78..911e6bc 100644 --- a/frontend/src/messages/es.json +++ b/frontend/src/messages/es.json @@ -68,7 +68,8 @@ "headers": { "label": "Cabeceras de correo", "desc": "SPF, DKIM, saltos de enrutamiento" }, "crypto": { "label": "Dirección cripto", "desc": "Bitcoin y Ethereum" }, "qr": { "label": "Decodificar QR", "desc": "Decodificar y analizar" }, - "mac": { "label": "Búsqueda MAC", "desc": "Búsqueda de proveedor" } + "mac": { "label": "Búsqueda MAC", "desc": "Búsqueda de proveedor" }, + "subnet": { "label": "IP / Subred", "desc": "Rangos de red & CIDR" } }, "demo": { "line1": "Esta es una demo pública con cuotas de API limitadas. Algunos módulos (Shodan, VirusTotal) pueden tener límite de velocidad.", diff --git a/frontend/src/messages/fr.json b/frontend/src/messages/fr.json index 15a5e45..8daad79 100644 --- a/frontend/src/messages/fr.json +++ b/frontend/src/messages/fr.json @@ -74,7 +74,8 @@ "headers": { "label": "En-têtes d'e-mail", "desc": "SPF, DKIM, sauts de routage" }, "crypto": { "label": "Adresse crypto", "desc": "Bitcoin et Ethereum" }, "qr": { "label": "Décodeur QR", "desc": "Décoder et analyser" }, - "mac": { "label": "Recherche MAC", "desc": "Recherche de fournisseur" } + "mac": { "label": "Recherche MAC", "desc": "Recherche de fournisseur" }, + "subnet": { "label": "IP / Sous-réseau", "desc": "Plages réseau & CIDR" } }, "demo": { "line1": "Ceci est une démo publique avec des quotas API limités. Certains modules (Shodan, VirusTotal) peuvent être soumis à des limites de débit.", diff --git a/frontend/src/messages/ru.json b/frontend/src/messages/ru.json index 1e3ed75..9707f2b 100644 --- a/frontend/src/messages/ru.json +++ b/frontend/src/messages/ru.json @@ -75,7 +75,8 @@ "headers": { "label": "Заголовки письма", "desc": "SPF, DKIM, маршрут" }, "crypto": { "label": "Крипто-адрес", "desc": "Bitcoin и Ethereum" }, "qr": { "label": "Декодер QR", "desc": "Декодирование и анализ" }, - "mac": { "label": "MAC Lookup", "desc": "Поиск производителя" } + "mac": { "label": "MAC Lookup", "desc": "Поиск производителя" }, + "subnet": { "label": "IP / Подсеть", "desc": "Диапазоны сети и CIDR" } }, "demo": { "line1": "Это публичное демо с ограниченными квотами API. Некоторые модули (Shodan, VirusTotal) могут быть ограничены частотой запросов.",