Skip to main content
Glama

firewalla-mcp-server

geographic.ts30.5 kB
/** * Unified geographic utilities for IP geolocation and enrichment * Combines functionality from geographic-cache.ts, geographic-constants.ts, and geographic-utils.ts */ import geoip from 'geoip-lite'; import type { GeographicData } from '../types.js'; /** * Private IP address patterns that should not be geolocated */ export const PRIVATE_IP_PATTERNS = [ /^10\./, // 10.0.0.0/8 /^192\.168\./, // 192.168.0.0/16 /^172\.(1[6-9]|2\d|3[01])\./, // 172.16.0.0/12 /^127\./, // 127.0.0.0/8 (localhost) /^169\.254\./, // 169.254.0.0/16 (link-local) /^::1$/, // IPv6 localhost /^fe80:/, // IPv6 link-local /^fc00:/, // IPv6 unique local /^fd00:/, // IPv6 unique local ] as const; /** * Mapping of country codes to continent names * Comprehensive list covering all major countries and territories */ export const COUNTRY_TO_CONTINENT: Record<string, string> = { // North America US: 'North America', CA: 'North America', MX: 'North America', GT: 'North America', BZ: 'North America', SV: 'North America', HN: 'North America', NI: 'North America', CR: 'North America', PA: 'North America', CU: 'North America', JM: 'North America', HT: 'North America', DO: 'North America', PR: 'North America', // South America BR: 'South America', AR: 'South America', CL: 'South America', PE: 'South America', CO: 'South America', VE: 'South America', EC: 'South America', BO: 'South America', PY: 'South America', UY: 'South America', GY: 'South America', SR: 'South America', GF: 'South America', FK: 'South America', // Europe GB: 'Europe', DE: 'Europe', FR: 'Europe', IT: 'Europe', ES: 'Europe', PT: 'Europe', NL: 'Europe', BE: 'Europe', LU: 'Europe', IE: 'Europe', AT: 'Europe', CH: 'Europe', PL: 'Europe', CZ: 'Europe', SK: 'Europe', HU: 'Europe', RO: 'Europe', BG: 'Europe', GR: 'Europe', HR: 'Europe', SI: 'Europe', RS: 'Europe', BA: 'Europe', ME: 'Europe', MK: 'Europe', AL: 'Europe', NO: 'Europe', SE: 'Europe', FI: 'Europe', DK: 'Europe', IS: 'Europe', EE: 'Europe', LV: 'Europe', LT: 'Europe', BY: 'Europe', UA: 'Europe', MD: 'Europe', RU: 'Europe', // Partly in Asia, but primarily European // Asia CN: 'Asia', JP: 'Asia', KR: 'Asia', IN: 'Asia', PK: 'Asia', BD: 'Asia', LK: 'Asia', NP: 'Asia', BT: 'Asia', MM: 'Asia', TH: 'Asia', LA: 'Asia', VN: 'Asia', KH: 'Asia', MY: 'Asia', SG: 'Asia', ID: 'Asia', PH: 'Asia', TL: 'Asia', BN: 'Asia', TW: 'Asia', HK: 'Asia', MO: 'Asia', MN: 'Asia', KZ: 'Asia', UZ: 'Asia', TM: 'Asia', KG: 'Asia', TJ: 'Asia', AF: 'Asia', IR: 'Asia', IQ: 'Asia', SA: 'Asia', YE: 'Asia', OM: 'Asia', AE: 'Asia', QA: 'Asia', BH: 'Asia', KW: 'Asia', JO: 'Asia', LB: 'Asia', SY: 'Asia', IL: 'Asia', PS: 'Asia', TR: 'Asia', GE: 'Asia', AM: 'Asia', AZ: 'Asia', // Africa EG: 'Africa', LY: 'Africa', TN: 'Africa', DZ: 'Africa', MA: 'Africa', EH: 'Africa', MR: 'Africa', ML: 'Africa', NE: 'Africa', TD: 'Africa', SD: 'Africa', SS: 'Africa', ER: 'Africa', DJ: 'Africa', SO: 'Africa', ET: 'Africa', KE: 'Africa', UG: 'Africa', TZ: 'Africa', RW: 'Africa', BI: 'Africa', MZ: 'Africa', MW: 'Africa', ZM: 'Africa', ZW: 'Africa', BW: 'Africa', NA: 'Africa', ZA: 'Africa', LS: 'Africa', SZ: 'Africa', MG: 'Africa', KM: 'Africa', KP: 'Asia', MU: 'Africa', SC: 'Africa', AO: 'Africa', CG: 'Africa', CD: 'Africa', GA: 'Africa', GQ: 'Africa', CF: 'Africa', CM: 'Africa', NG: 'Africa', BJ: 'Africa', TG: 'Africa', GH: 'Africa', CI: 'Africa', BF: 'Africa', LR: 'Africa', SL: 'Africa', GN: 'Africa', GW: 'Africa', SN: 'Africa', GM: 'Africa', CV: 'Africa', // Oceania AU: 'Oceania', NZ: 'Oceania', PG: 'Oceania', FJ: 'Oceania', SB: 'Oceania', NC: 'Oceania', PF: 'Oceania', VU: 'Oceania', WS: 'Oceania', KI: 'Oceania', TO: 'Oceania', TV: 'Oceania', NR: 'Oceania', PW: 'Oceania', MH: 'Oceania', FM: 'Oceania', // Antarctica AQ: 'Antarctica', } as const; /** * Mapping of country codes to regions for more granular geographic analysis */ export const COUNTRY_TO_REGION: Record<string, string> = { // North America US: 'Northern America', CA: 'Northern America', MX: 'Central America', GT: 'Central America', BZ: 'Central America', SV: 'Central America', HN: 'Central America', NI: 'Central America', CR: 'Central America', PA: 'Central America', CU: 'Caribbean', JM: 'Caribbean', HT: 'Caribbean', DO: 'Caribbean', PR: 'Caribbean', // Europe GB: 'Northern Europe', IE: 'Northern Europe', NO: 'Northern Europe', SE: 'Northern Europe', FI: 'Northern Europe', DK: 'Northern Europe', IS: 'Northern Europe', EE: 'Northern Europe', LV: 'Northern Europe', LT: 'Northern Europe', DE: 'Western Europe', FR: 'Western Europe', NL: 'Western Europe', BE: 'Western Europe', LU: 'Western Europe', AT: 'Western Europe', CH: 'Western Europe', ES: 'Southern Europe', PT: 'Southern Europe', IT: 'Southern Europe', GR: 'Southern Europe', HR: 'Southern Europe', SI: 'Southern Europe', RS: 'Southern Europe', BA: 'Southern Europe', ME: 'Southern Europe', MK: 'Southern Europe', AL: 'Southern Europe', PL: 'Eastern Europe', CZ: 'Eastern Europe', SK: 'Eastern Europe', HU: 'Eastern Europe', RO: 'Eastern Europe', BG: 'Eastern Europe', BY: 'Eastern Europe', UA: 'Eastern Europe', MD: 'Eastern Europe', RU: 'Eastern Europe', // Asia CN: 'Eastern Asia', JP: 'Eastern Asia', KR: 'Eastern Asia', TW: 'Eastern Asia', HK: 'Eastern Asia', MO: 'Eastern Asia', MN: 'Eastern Asia', IN: 'Southern Asia', PK: 'Southern Asia', BD: 'Southern Asia', LK: 'Southern Asia', NP: 'Southern Asia', BT: 'Southern Asia', AF: 'Southern Asia', IR: 'Southern Asia', TH: 'South-Eastern Asia', VN: 'South-Eastern Asia', MY: 'South-Eastern Asia', SG: 'South-Eastern Asia', ID: 'South-Eastern Asia', PH: 'South-Eastern Asia', MM: 'South-Eastern Asia', LA: 'South-Eastern Asia', KH: 'South-Eastern Asia', BN: 'South-Eastern Asia', TL: 'South-Eastern Asia', SA: 'Western Asia', YE: 'Western Asia', OM: 'Western Asia', AE: 'Western Asia', QA: 'Western Asia', BH: 'Western Asia', KW: 'Western Asia', IQ: 'Western Asia', JO: 'Western Asia', LB: 'Western Asia', SY: 'Western Asia', IL: 'Western Asia', PS: 'Western Asia', TR: 'Western Asia', GE: 'Western Asia', AM: 'Western Asia', AZ: 'Western Asia', KZ: 'Central Asia', UZ: 'Central Asia', TM: 'Central Asia', KG: 'Central Asia', TJ: 'Central Asia', // Africa EG: 'Northern Africa', LY: 'Northern Africa', TN: 'Northern Africa', DZ: 'Northern Africa', MA: 'Northern Africa', EH: 'Northern Africa', SD: 'Northern Africa', SS: 'Eastern Africa', ER: 'Eastern Africa', DJ: 'Eastern Africa', SO: 'Eastern Africa', ET: 'Eastern Africa', KE: 'Eastern Africa', UG: 'Eastern Africa', TZ: 'Eastern Africa', RW: 'Eastern Africa', BI: 'Eastern Africa', MG: 'Eastern Africa', KM: 'Eastern Africa', MU: 'Eastern Africa', SC: 'Eastern Africa', MZ: 'Eastern Africa', MW: 'Eastern Africa', ZM: 'Eastern Africa', ZW: 'Eastern Africa', AO: 'Middle Africa', CG: 'Middle Africa', CD: 'Middle Africa', GA: 'Middle Africa', GQ: 'Middle Africa', CF: 'Middle Africa', CM: 'Middle Africa', TD: 'Middle Africa', BW: 'Southern Africa', NA: 'Southern Africa', ZA: 'Southern Africa', LS: 'Southern Africa', SZ: 'Southern Africa', NG: 'Western Africa', BJ: 'Western Africa', TG: 'Western Africa', GH: 'Western Africa', CI: 'Western Africa', BF: 'Western Africa', ML: 'Western Africa', NE: 'Western Africa', MR: 'Western Africa', LR: 'Western Africa', SL: 'Western Africa', GN: 'Western Africa', GW: 'Western Africa', SN: 'Western Africa', GM: 'Western Africa', CV: 'Western Africa', // Oceania AU: 'Australia and New Zealand', NZ: 'Australia and New Zealand', PG: 'Melanesia', FJ: 'Melanesia', SB: 'Melanesia', NC: 'Melanesia', VU: 'Melanesia', PF: 'Polynesia', WS: 'Polynesia', TO: 'Polynesia', TV: 'Polynesia', KI: 'Micronesia', NR: 'Micronesia', PW: 'Micronesia', MH: 'Micronesia', FM: 'Micronesia', } as const; /** * Country risk scores for security analysis * Higher scores indicate higher risk based on cybersecurity threat data */ export const COUNTRY_RISK_SCORES: Record<string, number> = { // High-risk countries (0.8-1.0) CN: 0.95, RU: 0.95, KP: 1.0, IR: 0.9, SY: 0.85, VE: 0.8, CU: 0.8, // Medium-risk countries (0.5-0.7) IN: 0.6, BR: 0.6, TR: 0.65, UA: 0.7, RO: 0.65, VN: 0.6, ID: 0.55, EG: 0.6, PK: 0.65, BD: 0.6, NG: 0.65, ZA: 0.6, KE: 0.55, AR: 0.55, MX: 0.6, // Low-risk countries (0.1-0.4) US: 0.3, CA: 0.2, GB: 0.25, DE: 0.2, FR: 0.25, JP: 0.15, AU: 0.2, NZ: 0.15, SG: 0.2, KR: 0.25, NL: 0.2, CH: 0.15, SE: 0.15, NO: 0.15, DK: 0.15, FI: 0.15, // Default for unlisted countries DEFAULT: 0.5, } as const; /** * Cache configuration for geographic data */ export const CACHE_CONFIG = { maxSize: 10000, ttlMs: 3600000, // 1 hour enableStats: true, } as const; /** * Geographic cache entry interface */ export interface GeographicCacheEntry { data: GeographicData | null; timestamp: number; } /** * Geographic cache configuration */ export interface GeographicCacheConfig { maxSize: number; ttlMs: number; enableStats: boolean; } /** * Geographic cache statistics */ export interface GeographicCacheStats { size: number; maxSize: number; hitCount: number; missCount: number; hitRate: number; evictionCount: number; } /** * Geographic cache implementation with LRU eviction */ export class GeographicCache { private cache = new Map<string, GeographicCacheEntry>(); private stats: GeographicCacheStats = { size: 0, maxSize: CACHE_CONFIG.maxSize, hitCount: 0, missCount: 0, hitRate: 0, evictionCount: 0, }; private config: GeographicCacheConfig = { ...CACHE_CONFIG }; constructor(config?: Partial<GeographicCacheConfig>) { if (config) { this.config = { ...this.config, ...config }; this.stats.maxSize = this.config.maxSize; } } /** * Get cached geographic data for an IP */ get(ip: string): GeographicData | null | undefined { const entry = this.cache.get(ip); if (!entry) { if (this.config.enableStats) { this.stats.missCount++; this.updateHitRate(); } return undefined; } // Check if entry is expired const now = Date.now(); if (now - entry.timestamp > this.config.ttlMs) { this.cache.delete(ip); if (this.config.enableStats) { this.stats.missCount++; this.updateHitRate(); } return undefined; } // Move to end for LRU this.cache.delete(ip); this.cache.set(ip, entry); if (this.config.enableStats) { this.stats.hitCount++; this.updateHitRate(); } return entry.data; } /** * Set geographic data for an IP */ set(ip: string, data: GeographicData | null): void { // Check if we need to evict if (this.cache.size >= this.config.maxSize && !this.cache.has(ip)) { // Remove oldest entry (first in map) const firstKey = this.cache.keys().next().value; if (firstKey) { this.cache.delete(firstKey); if (this.config.enableStats) { this.stats.evictionCount++; } } } this.cache.set(ip, { data, timestamp: Date.now(), }); this.stats.size = this.cache.size; } /** * Clear the cache */ clear(): void { this.cache.clear(); this.stats = { size: 0, maxSize: this.config.maxSize, hitCount: 0, missCount: 0, hitRate: 0, evictionCount: 0, }; } /** * Get cache statistics */ getStats(): GeographicCacheStats { return { ...this.stats }; } /** * Remove expired entries */ pruneExpired(): number { const now = Date.now(); let removedCount = 0; for (const [ip, entry] of this.cache.entries()) { if (now - entry.timestamp > this.config.ttlMs) { this.cache.delete(ip); removedCount++; } } this.stats.size = this.cache.size; return removedCount; } /** * Get current configuration */ getConfig(): GeographicCacheConfig { return { ...this.config }; } /** * Update cache configuration */ updateConfig(newConfig: Partial<GeographicCacheConfig>): void { this.config = { ...this.config, ...newConfig, }; // If max size reduced, remove excess entries if (this.cache.size > this.config.maxSize) { const entriesToRemove = this.cache.size - this.config.maxSize; const entries = Array.from(this.cache.keys()); for (let i = 0; i < entriesToRemove; i++) { this.cache.delete(entries[i]); if (this.config.enableStats) { this.stats.evictionCount++; } } } } private updateHitRate(): void { const total = this.stats.hitCount + this.stats.missCount; this.stats.hitRate = total > 0 ? this.stats.hitCount / total : 0; } } /** * Check if an IP is a private/internal IP address */ export function isPrivateIP(ip: string): boolean { return PRIVATE_IP_PATTERNS.some(pattern => pattern.test(ip)); } /** * Map a country code to its continent */ export function mapContinent(countryCode: string): string { return COUNTRY_TO_CONTINENT[countryCode?.toUpperCase()] || 'Unknown'; } /** * Calculate risk score for a country (0-10 scale) */ export function calculateRiskScore(countryCode: string): number { const score = COUNTRY_RISK_SCORES[countryCode?.toUpperCase()] || COUNTRY_RISK_SCORES.DEFAULT; // Convert from 0-1 scale to 0-10 scale return score * 10; } /** * Get geographic data for an IP address */ export function getGeographicDataForIP(ip: string): GeographicData | null { // Check if private IP if (!ip || isPrivateIP(ip)) { return null; } try { const geo = geoip.lookup(ip); if (!geo) { return null; } return { country: geo.country || 'Unknown', country_code: geo.country || 'UN', continent: mapContinent(geo.country), region: geo.region || 'Unknown', city: geo.city || 'Unknown', timezone: geo.timezone || 'UTC', geographic_risk_score: calculateRiskScore(geo.country), }; } catch (_error) { return null; } } /** * Enhanced IP validation with support for IPv6 */ export function isValidIPv6(ip: string): boolean { if (!ip || typeof ip !== 'string') { return false; } // Comprehensive IPv6 pattern const ipv6Pattern = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; return ipv6Pattern.test(ip); } /** * Enhanced geographic data provider with fallback mechanisms */ export function getEnhancedGeographicDataForIP( ip: string ): GeographicData | null { // Check if private IP if (!ip || isPrivateIP(ip)) { return null; } // Normalize IP address const normalizedIP = normalizeIP(ip); if (!normalizedIP) { return null; } try { // Primary: geoip-lite lookup const geo = geoip.lookup(normalizedIP); if (geo && geo.country) { return { country: geo.country || 'Unknown', country_code: geo.country || 'UN', continent: mapContinent(geo.country), region: geo.region || 'Unknown', city: geo.city || 'Unknown', timezone: geo.timezone || 'UTC', geographic_risk_score: calculateRiskScore(geo.country), }; } // Fallback: IP range detection for known blocks const rangeData = detectIPRange(normalizedIP); if (rangeData) { return rangeData; } // Final fallback: Geographic inference from IP structure return inferGeographicFromIP(normalizedIP); } catch (_error) { // Return basic inference as last resort return inferGeographicFromIP(normalizedIP); } } /** * Detect geographic data from known IP ranges */ function detectIPRange(ip: string): GeographicData | null { const parts = ip.split('.'); if (parts.length !== 4) { return null; } const firstTwoOctets = `${parts[0]}.${parts[1]}`; // Known cloud provider ranges const cloudProviderRanges: Record<string, Partial<GeographicData>> = { // AWS ranges '54.': { country: 'United States', country_code: 'US', continent: 'North America', is_cloud_provider: true, isp: 'Amazon', }, '52.': { country: 'United States', country_code: 'US', continent: 'North America', is_cloud_provider: true, isp: 'Amazon', }, '3.': { country: 'United States', country_code: 'US', continent: 'North America', is_cloud_provider: true, isp: 'Amazon', }, '18.': { country: 'United States', country_code: 'US', continent: 'North America', is_cloud_provider: true, isp: 'Amazon', }, // Google ranges '8.8': { country: 'United States', country_code: 'US', continent: 'North America', is_cloud_provider: true, isp: 'Google', }, '8.34': { country: 'United States', country_code: 'US', continent: 'North America', is_cloud_provider: true, isp: 'Google', }, '8.35': { country: 'United States', country_code: 'US', continent: 'North America', is_cloud_provider: true, isp: 'Google', }, // Cloudflare ranges '1.1': { country: 'United States', country_code: 'US', continent: 'North America', is_cloud_provider: true, isp: 'Cloudflare', }, '1.0': { country: 'United States', country_code: 'US', continent: 'North America', is_cloud_provider: true, isp: 'Cloudflare', }, // Microsoft Azure ranges '13.': { country: 'United States', country_code: 'US', continent: 'North America', is_cloud_provider: true, isp: 'Microsoft', }, '20.': { country: 'United States', country_code: 'US', continent: 'North America', is_cloud_provider: true, isp: 'Microsoft', }, '40.': { country: 'United States', country_code: 'US', continent: 'North America', is_cloud_provider: true, isp: 'Microsoft', }, // European ranges '46.': { country: 'Germany', country_code: 'DE', continent: 'Europe', region: 'Western Europe', }, '85.': { country: 'Germany', country_code: 'DE', continent: 'Europe', region: 'Western Europe', }, '185.': { country: 'United Kingdom', country_code: 'GB', continent: 'Europe', region: 'Northern Europe', }, // Asian ranges '202.': { country: 'China', country_code: 'CN', continent: 'Asia', region: 'Eastern Asia', }, '218.': { country: 'China', country_code: 'CN', continent: 'Asia', region: 'Eastern Asia', }, '58.': { country: 'China', country_code: 'CN', continent: 'Asia', region: 'Eastern Asia', }, }; // Check two-octet patterns first if (cloudProviderRanges[firstTwoOctets]) { const rangeData = cloudProviderRanges[firstTwoOctets]; return { country: rangeData.country || 'Unknown', country_code: rangeData.country_code || 'UN', continent: rangeData.continent || 'Unknown', region: rangeData.region || 'Unknown', city: 'Unknown', timezone: 'UTC', isp: rangeData.isp, is_cloud_provider: rangeData.is_cloud_provider || false, geographic_risk_score: calculateRiskScore(rangeData.country_code || 'UN'), }; } // Check single-octet patterns const firstOctet = parts[0]; if (cloudProviderRanges[`${firstOctet}.`]) { const rangeData = cloudProviderRanges[`${firstOctet}.`]; return { country: rangeData.country || 'Unknown', country_code: rangeData.country_code || 'UN', continent: rangeData.continent || 'Unknown', region: rangeData.region || 'Unknown', city: 'Unknown', timezone: 'UTC', isp: rangeData.isp, is_cloud_provider: rangeData.is_cloud_provider || false, geographic_risk_score: calculateRiskScore(rangeData.country_code || 'UN'), }; } return null; } /** * Infer basic geographic data from IP structure (last resort) */ function inferGeographicFromIP(ip: string): GeographicData | null { const parts = ip.split('.'); if (parts.length !== 4) { return null; } const firstOctet = parseInt(parts[0], 10); if (isNaN(firstOctet)) { return null; } // Very basic geographic inference based on IANA allocations let countryCode = 'US'; // Default let continent = 'North America'; let region = 'Unknown'; // IANA IP allocation regions (simplified) if (firstOctet >= 1 && firstOctet <= 126) { // Historically mostly North America countryCode = 'US'; continent = 'North America'; region = 'Northern America'; } else if (firstOctet >= 128 && firstOctet <= 191) { // Class B - mixed allocation countryCode = 'US'; continent = 'North America'; region = 'Northern America'; } else if (firstOctet >= 192 && firstOctet <= 223) { // Class C - more international countryCode = 'GB'; continent = 'Europe'; region = 'Northern Europe'; } else if (firstOctet >= 224 && firstOctet <= 239) { // Multicast - treat as unknown countryCode = 'UN'; continent = 'Unknown'; region = 'Unknown'; } return { country: COUNTRY_TO_CONTINENT[countryCode] || 'Unknown', country_code: countryCode, continent, region, city: 'Unknown', timezone: 'UTC', geographic_risk_score: calculateRiskScore(countryCode), }; } /** * Enrich an object with geographic data based on IP fields * Enhanced version with fallback mechanisms */ export function enrichObjectWithGeo<T extends Record<string, any>>( obj: T, ipFields: string[] = ['source_ip', 'destination_ip', 'device_ip', 'ip'] ): T & Record<string, GeographicData | null> { const enriched: any = { ...obj }; for (const field of ipFields) { const ip = obj[field]; if (ip && typeof ip === 'string') { const geoField = `${field}_geo`; if (!enriched[geoField]) { // Use enhanced geographic data provider enriched[geoField] = getEnhancedGeographicDataForIP(ip); } } } return enriched; } /** * Batch enrich multiple objects efficiently */ export function enrichObjectsWithGeoBatch<T extends Record<string, any>>( objects: T[], ipFields: string[] = ['source_ip', 'destination_ip', 'device_ip', 'ip'] ): Array<T & Record<string, GeographicData | null>> { return objects.map(obj => enrichObjectWithGeo(obj, ipFields)); } /** * Validate IP address format */ export function isValidIP(ip: string): boolean { if (!ip || typeof ip !== 'string') { return false; } // IPv4 pattern const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/; if (ipv4Pattern.test(ip)) { const parts = ip.split('.'); return parts.every(part => { const num = parseInt(part, 10); return num >= 0 && num <= 255; }); } // IPv6 pattern (simplified) const ipv6Pattern = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/; return ipv6Pattern.test(ip); } /** * Normalize IP address */ export function normalizeIP(ip: string): string | null { if (!isValidIP(ip)) { return null; } // For IPv4, remove leading zeros if (ip.includes('.')) { return ip .split('.') .map(part => parseInt(part, 10).toString()) .join('.'); } // For IPv6, lowercase return ip.toLowerCase(); } /** * Check if a country code is valid */ export function isValidCountryCode(countryCode: string): boolean { return ( typeof countryCode === 'string' && countryCode.length === 2 && countryCode.toUpperCase() in COUNTRY_TO_CONTINENT ); } /** * Validate multiple country codes */ export function validateCountryCodes(countryCodes: string[]): { valid: string[]; invalid: string[]; } { const valid: string[] = []; const invalid: string[] = []; for (const code of countryCodes) { if (isValidCountryCode(code)) { valid.push(code.toUpperCase()); } else { invalid.push(code); } } return { valid, invalid }; } /** * Filter results by geographic criteria * Works entirely client-side without API calls */ export function filterByGeography<T extends Record<string, any>>( results: T[], geoFilters: { countries?: string[]; continents?: string[]; regions?: string[]; cities?: string[]; asns?: string[]; hosting_providers?: string[]; exclude_cloud?: boolean; exclude_vpn?: boolean; min_risk_score?: number; high_risk_countries?: boolean; exclude_known_providers?: boolean; } ): T[] { if (!results || results.length === 0) { return results; } // If no filters specified, return all results if (!geoFilters || Object.keys(geoFilters).length === 0) { return results; } return results.filter(result => { // Check all IP fields for geographic data const ipFields = ['source_ip', 'destination_ip', 'device_ip', 'ip']; let hasMatchingGeo = false; for (const field of ipFields) { const ip = result[field]; if (!ip) { continue; } // Get geographic data for this IP const geoData = getEnhancedGeographicDataForIP(ip); if (!geoData) { continue; } // Check country filter (OR logic for multiple values) if (geoFilters.countries && geoFilters.countries.length > 0) { const countryMatch = geoFilters.countries.some(country => { return ( geoData.country?.toLowerCase() === country.toLowerCase() || geoData.country_code?.toLowerCase() === country.toLowerCase() ); }); if (!countryMatch) { continue; } } // Check continent filter (OR logic for multiple values) if (geoFilters.continents && geoFilters.continents.length > 0) { const continentMatch = geoFilters.continents.some(continent => { return geoData.continent?.toLowerCase() === continent.toLowerCase(); }); if (!continentMatch) { continue; } } // Check region filter (OR logic for multiple values) if (geoFilters.regions && geoFilters.regions.length > 0) { const regionMatch = geoFilters.regions.some(region => { return geoData.region?.toLowerCase() === region.toLowerCase(); }); if (!regionMatch) { continue; } } // Check city filter (OR logic for multiple values) if (geoFilters.cities && geoFilters.cities.length > 0) { const cityMatch = geoFilters.cities.some(city => { return geoData.city?.toLowerCase() === city.toLowerCase(); }); if (!cityMatch) { continue; } } // Check ASN filter (OR logic for multiple values) if (geoFilters.asns && geoFilters.asns.length > 0) { const asnMatch = geoFilters.asns.some(asn => { return geoData.asn?.toString() === asn.toString(); }); if (!asnMatch) { continue; } } // Check hosting provider filter (OR logic for multiple values) if ( geoFilters.hosting_providers && geoFilters.hosting_providers.length > 0 ) { const providerMatch = geoFilters.hosting_providers.some(provider => { return ( geoData.isp?.toLowerCase().includes(provider.toLowerCase()) || geoData.organization?.toLowerCase().includes(provider.toLowerCase()) ); }); if (!providerMatch) { continue; } } // Check cloud provider exclusion if (geoFilters.exclude_cloud && geoData.is_cloud_provider) { continue; } // Check VPN exclusion if (geoFilters.exclude_vpn && geoData.is_vpn) { continue; } // Check minimum risk score if ( geoFilters.min_risk_score !== undefined && geoFilters.min_risk_score > 0 ) { const riskScore = geoData.geographic_risk_score || 0; if (riskScore < geoFilters.min_risk_score) { continue; } } // Check high-risk countries flag if (geoFilters.high_risk_countries) { const riskScore = geoData.geographic_risk_score || 0; if (riskScore < 8) { continue; } // High risk is 8+ on 0-10 scale } // Check known providers exclusion if (geoFilters.exclude_known_providers) { const knownProviders = [ 'amazon', 'google', 'microsoft', 'cloudflare', 'akamai', ]; const isKnownProvider = knownProviders.some(provider => { const isp = geoData.isp?.toLowerCase() || ''; const org = geoData.organization?.toLowerCase() || ''; return isp.includes(provider) || org.includes(provider); }); if (isKnownProvider) { continue; } } // If we made it here, this IP matches all filters hasMatchingGeo = true; break; } return hasMatchingGeo; }); } /** * Global geographic cache instance * Used across the application for consistent geographic data caching */ export const geoCache = new GeographicCache();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/amittell/firewalla-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server