import { getTrustedClientIpOrNull } from "~/utils/server/IpUtils"; import { IpAddressDto } from "../dtos/IpAddressDto"; import { cachified, clearCacheKey } from "~/utils/cache.server"; import { addBlacklistAttempt, addToBlacklist, findInBlacklist } from "~/utils/db/blacklist.db.server"; import { createScopedLogger } from "~/utils/logger.server"; import type { IpAddress } from "@hivegrid/db/prisma-browser"; import { appendIpAddressLogCapability, findIpAddressCapability, upsertIpAddressCapability } from "@hivegrid/db/pretenant-security.server"; import { hashIp, lookupCountry } from "./geoip.server"; const ipAddressLogger = createScopedLogger("ipAddress"); const LOCAL_REQUEST_IP = "127.0.0.1"; type IpAddressLogErrorCode = "blacklisted" | "unauthorized"; async function log( request: Request, { action, description, metadata, block, }: { action: string; description: string; metadata?: { [key: string]: any }; block?: IpAddressLogErrorCode; } ) { const ip = getTrustedClientIpOrNull(request) || LOCAL_REQUEST_IP; // AD-22: Persist only the pseudonymous hash, never the plain-text IP address. const storedIp = hashIp(ip); const ipAddress = await getOrCreateIpAddressLookup(ip); if (!ipAddress) { return; } const validationError = getIpAddressValidationError(ipAddress); // Match the runtime security list against the raw IP without persisting it in logs. let blacklistedIp = await findInBlacklist("ip", ip); if (!blacklistedIp && block) { blacklistedIp = await addToBlacklist({ type: "ip", value: ip }); } const error = blacklistedIp ? "blacklisted" : (validationError ?? block ?? null); if (error) { if (blacklistedIp) { await addBlacklistAttempt(blacklistedIp); } await appendIpAddressLogCapability({ ipAddressId: ipAddress.id, ip: storedIp, url: new URL(request.url).pathname, action, description, metadata: null, error, success: false, }); throw Error(error); } else { await appendIpAddressLogCapability({ ipAddressId: ipAddress.id || null, ip: storedIp, url: new URL(request.url).pathname, action, description, metadata: metadata ? JSON.stringify(metadata, null, 2) : null, error: null, success: true, }); } } async function getOrCreateIpAddressLookup(ip: string): Promise { // AD-22: Use the pseudonymous hash as key and resolve the country locally with DB-IP Lite. const ipHash = hashIp(ip); const cacheKey = `ipAddress:${ipHash}`; const item = await cachified({ key: cacheKey, disabled: isLocalRequestIp(ip), getFreshValue: () => findIpAddressCapability(ipHash), }); if (item) { return toIpAddressDto(item); } const geo = await lookupCountry(ip); const createdIp = await upsertIpAddressCapability({ ip: ipHash, provider: geo ? "local-db" : "", type: "", countryCode: geo?.countryCode ?? "", countryName: geo?.countryName ?? "", regionCode: "", regionName: "", city: "", zipCode: "", latitude: null, longitude: null, metadata: JSON.stringify(null), }); clearCacheKey(cacheKey); return toIpAddressDto(createdIp); } function isLocalRequestIp(ip: string): boolean { return ip === LOCAL_REQUEST_IP || ip === "::1"; } function toIpAddressDto(item: IpAddress): IpAddressDto | null { try { return { id: item.id, ip: item.ip, provider: item.provider, type: item.type, countryCode: item.countryCode, countryName: item.countryName, regionCode: item.regionCode, regionName: item.regionName, city: item.city, zipCode: item.zipCode, latitude: item.latitude ? Number(item.latitude) : null, longitude: item.longitude ? Number(item.longitude) : null, metadata: JSON.parse(item.metadata), }; } catch (error: any) { ipAddressLogger.error({ err: error }, "[getOrCreateIpAddressLookup] Error parsing metadata"); return null; } } function getIpAddressValidationError(ipAddress: IpAddressDto): IpAddressLogErrorCode | null { if (!ipAddress.metadata) { return null; } if (ipAddress.provider === "ipapi.is") { let data: IpApiIsDto = ipAddress.metadata as IpApiIsDto; ipAddressLogger.debug( { is_bogon: data.is_bogon, is_mobile: data.is_mobile, is_crawler: data.is_crawler, is_datacenter: data.is_datacenter, is_tor: data.is_tor, is_proxy: data.is_proxy, is_vpn: data.is_vpn, is_abuser: data.is_abuser, }, "[validateIp]" ); if (data.is_crawler) { return "unauthorized"; } if (data.is_abuser) { return "unauthorized"; } if (data.location) { const blockCountryCodes: string[] = []; if (blockCountryCodes.includes(data.location.country_code)) { return "unauthorized"; } } } return null; } /** * Threat-flag shape from the former ipapi.is provider. It remains the contract for legacy records; * new local country lookups no longer persist provider metadata (AD-22). */ type IpApiIsDto = { ip: string; rir: string; is_bogon: boolean; is_mobile: boolean; is_crawler: boolean; is_datacenter: boolean; is_tor: boolean; is_proxy: boolean; is_vpn: boolean; is_abuser: boolean; location?: { country: string; country_code: string; city: string; latitude: number; longitude: number; zip: string; }; }; export default { log, getOrCreateIpAddressLookup, };