feat(ipintelligence): add canonical RDAP CIDR coverage and derive CIDRs from IPv4 start/end ranges

This commit is contained in:
2026-04-26 20:45:47 +00:00
parent 5331a3c2ce
commit 3e86e99d4f
5 changed files with 139 additions and 11 deletions
+97 -9
View File
@@ -30,6 +30,7 @@ export interface IIpIntelligenceResult {
registrantOrg: string | null;
registrantCountry: string | null;
networkRange: string | null;
networkCidrs: string[] | null;
abuseContact: string | null;
// Geolocation (MaxMind GeoLite2 City)
@@ -79,6 +80,11 @@ interface IBootstrapEntry {
baseUrl: string;
}
interface IRdapNetworkInfo {
networkRange: string | null;
networkCidrs: string[] | null;
}
/**
* IpIntelligence provides IP address intelligence by combining three data sources:
* - RDAP (direct to RIRs) for registration/org data
@@ -120,6 +126,7 @@ export class IpIntelligence {
registrantOrg: null,
registrantCountry: null,
networkRange: null,
networkCidrs: null,
abuseContact: null,
country: null,
countryCode: null,
@@ -143,6 +150,7 @@ export class IpIntelligence {
result.registrantOrg = rdap.registrantOrg;
result.registrantCountry = rdap.registrantCountry;
result.networkRange = rdap.networkRange;
result.networkCidrs = rdap.networkCidrs;
result.abuseContact = rdap.abuseContact;
}
@@ -257,6 +265,7 @@ export class IpIntelligence {
registrantOrg: string | null;
registrantCountry: string | null;
networkRange: string | null;
networkCidrs: string[] | null;
abuseContact: string | null;
} | null> {
await this.ensureBootstrap();
@@ -280,14 +289,7 @@ export class IpIntelligence {
let registrantCountry: string | null = data.country || null;
let abuseContact: string | null = null;
// Parse network range
let networkRange: string | null = null;
if (data.cidr0_cidrs && data.cidr0_cidrs.length > 0) {
const cidr = data.cidr0_cidrs[0];
networkRange = `${cidr.v4prefix || cidr.v6prefix}/${cidr.length}`;
} else if (data.startAddress && data.endAddress) {
networkRange = `${data.startAddress} - ${data.endAddress}`;
}
const { networkRange, networkCidrs } = this.parseRdapNetworkInfo(data);
// Parse entities
if (data.entities && Array.isArray(data.entities)) {
@@ -320,7 +322,7 @@ export class IpIntelligence {
}
}
return { registrantOrg, registrantCountry, networkRange, abuseContact };
return { registrantOrg, registrantCountry, networkRange, networkCidrs, abuseContact };
} catch (err: any) {
this.logger.debug?.(`RDAP query failed for ${ip}: ${err.message}`);
return null;
@@ -329,6 +331,40 @@ export class IpIntelligence {
}
}
private parseRdapNetworkInfo(data: any): IRdapNetworkInfo {
const cidrs = this.extractRdapCidrs(data);
if (cidrs.length > 0) {
return {
networkRange: cidrs[0],
networkCidrs: cidrs,
};
}
if (typeof data.startAddress === 'string' && typeof data.endAddress === 'string') {
const rangeCidrs = this.ipv4RangeToCidrs(data.startAddress, data.endAddress);
return {
networkRange: rangeCidrs.length === 1
? rangeCidrs[0]
: `${data.startAddress} - ${data.endAddress}`,
networkCidrs: rangeCidrs.length > 0 ? rangeCidrs : null,
};
}
return { networkRange: null, networkCidrs: null };
}
private extractRdapCidrs(data: any): string[] {
if (!Array.isArray(data.cidr0_cidrs)) return [];
return data.cidr0_cidrs
.map((cidr: any) => {
const prefix = cidr?.v4prefix || cidr?.v6prefix;
const length = Number(cidr?.length);
if (typeof prefix !== 'string' || !Number.isInteger(length)) return null;
return `${prefix}/${length}`;
})
.filter(Boolean) as string[];
}
/**
* Extract the 'fn' (formatted name) from an entity's vcardArray
*/
@@ -573,4 +609,56 @@ export class IpIntelligence {
parseInt(parts[3], 10)) >>> 0
);
}
private ipv4RangeToCidrs(startIp: string, endIp: string): string[] {
const start = this.ipv4ToBigInt(startIp);
const end = this.ipv4ToBigInt(endIp);
if (start === undefined || end === undefined || start > end) return [];
const cidrs: string[] = [];
let current = start;
while (current <= end) {
let maxBlockSize = current === 0n ? 1n << 32n : current & -current;
const remaining = end - current + 1n;
while (maxBlockSize > remaining) {
maxBlockSize = maxBlockSize / 2n;
}
const prefixLength = 32 - this.powerOfTwoExponent(maxBlockSize);
cidrs.push(`${this.numberToIpv4(current)}/${prefixLength}`);
current += maxBlockSize;
}
return cidrs;
}
private ipv4ToBigInt(ip: string): bigint | undefined {
const parts = ip.trim().split('.');
if (parts.length !== 4) return undefined;
let result = 0n;
for (const part of parts) {
if (!/^\d+$/.test(part)) return undefined;
const number = Number(part);
if (!Number.isInteger(number) || number < 0 || number > 255) return undefined;
result = (result * 256n) + BigInt(number);
}
return result;
}
private numberToIpv4(value: bigint): string {
return [
Number((value >> 24n) & 255n),
Number((value >> 16n) & 255n),
Number((value >> 8n) & 255n),
Number(value & 255n),
].join('.');
}
private powerOfTwoExponent(value: bigint): number {
let exponent = 0;
let remaining = value;
while (remaining > 1n) {
remaining >>= 1n;
exponent++;
}
return exponent;
}
}