feat(ipintelligence): add canonical RDAP CIDR coverage and derive CIDRs from IPv4 start/end ranges
This commit is contained in:
@@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-04-26 - 4.7.0 - feat(ipintelligence)
|
||||||
|
add canonical RDAP CIDR coverage and derive CIDRs from IPv4 start/end ranges
|
||||||
|
|
||||||
|
- Expose a new networkCidrs field alongside networkRange in IP intelligence results
|
||||||
|
- Convert RDAP IPv4 start/end ranges into canonical CIDR blocks, preserving legacy range formatting when multiple prefixes are required
|
||||||
|
- Add tests covering single-prefix and multi-prefix RDAP range parsing
|
||||||
|
|
||||||
## 2026-04-13 - 4.6.0 - feat(domain-intelligence)
|
## 2026-04-13 - 4.6.0 - feat(domain-intelligence)
|
||||||
add domain intelligence lookups with RDAP and DNS enrichment
|
add domain intelligence lookups with RDAP and DNS enrichment
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ console.log(intel);
|
|||||||
// registrantOrg: 'Google LLC',
|
// registrantOrg: 'Google LLC',
|
||||||
// registrantCountry: 'United States',
|
// registrantCountry: 'United States',
|
||||||
// networkRange: '8.8.8.0/24',
|
// networkRange: '8.8.8.0/24',
|
||||||
|
// networkCidrs: ['8.8.8.0/24'],
|
||||||
// abuseContact: null,
|
// abuseContact: null,
|
||||||
// country: null,
|
// country: null,
|
||||||
// countryCode: 'US',
|
// countryCode: 'US',
|
||||||
@@ -109,7 +110,8 @@ interface IIpIntelligenceResult {
|
|||||||
// Registration (RDAP)
|
// Registration (RDAP)
|
||||||
registrantOrg: string | null;
|
registrantOrg: string | null;
|
||||||
registrantCountry: string | null;
|
registrantCountry: string | null;
|
||||||
networkRange: string | null; // CIDR or range
|
networkRange: string | null; // primary CIDR, or legacy start-end range when multiple CIDRs are needed
|
||||||
|
networkCidrs: string[] | null; // canonical CIDR coverage for the RDAP network when available
|
||||||
abuseContact: string | null; // abuse email from RDAP
|
abuseContact: string | null; // abuse email from RDAP
|
||||||
|
|
||||||
// Geolocation (MaxMind GeoLite2)
|
// Geolocation (MaxMind GeoLite2)
|
||||||
|
|||||||
@@ -41,6 +41,37 @@ tap.test('should get IP intelligence for 1.1.1.1 (Cloudflare)', async () => {
|
|||||||
// Note: 1.1.1.1 is anycast — city-level geo may be null in GeoLite2
|
// Note: 1.1.1.1 is anycast — city-level geo may be null in GeoLite2
|
||||||
});
|
});
|
||||||
|
|
||||||
|
tap.test('should derive a single CIDR from RDAP start/end ranges', async () => {
|
||||||
|
const intelligence = new smartnetwork.IpIntelligence();
|
||||||
|
const result = (intelligence as any).parseRdapNetworkInfo({
|
||||||
|
startAddress: '203.0.113.0',
|
||||||
|
endAddress: '203.0.113.255',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
networkRange: '203.0.113.0/24',
|
||||||
|
networkCidrs: ['203.0.113.0/24'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should expose CIDRs for RDAP ranges that need multiple prefixes', async () => {
|
||||||
|
const intelligence = new smartnetwork.IpIntelligence();
|
||||||
|
const result = (intelligence as any).parseRdapNetworkInfo({
|
||||||
|
startAddress: '203.0.113.5',
|
||||||
|
endAddress: '203.0.113.10',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
networkRange: '203.0.113.5 - 203.0.113.10',
|
||||||
|
networkCidrs: [
|
||||||
|
'203.0.113.5/32',
|
||||||
|
'203.0.113.6/31',
|
||||||
|
'203.0.113.8/31',
|
||||||
|
'203.0.113.10/32',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
tap.test('should get IP intelligence for own public IP', async () => {
|
tap.test('should get IP intelligence for own public IP', async () => {
|
||||||
const ips = await testSmartNetwork.getPublicIps();
|
const ips = await testSmartNetwork.getPublicIps();
|
||||||
if (ips.v4) {
|
if (ips.v4) {
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartnetwork',
|
name: '@push.rocks/smartnetwork',
|
||||||
version: '4.6.0',
|
version: '4.7.0',
|
||||||
description: 'A toolkit for network diagnostics including speed tests, port availability checks, and more.'
|
description: 'A toolkit for network diagnostics including speed tests, port availability checks, and more.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export interface IIpIntelligenceResult {
|
|||||||
registrantOrg: string | null;
|
registrantOrg: string | null;
|
||||||
registrantCountry: string | null;
|
registrantCountry: string | null;
|
||||||
networkRange: string | null;
|
networkRange: string | null;
|
||||||
|
networkCidrs: string[] | null;
|
||||||
abuseContact: string | null;
|
abuseContact: string | null;
|
||||||
|
|
||||||
// Geolocation (MaxMind GeoLite2 City)
|
// Geolocation (MaxMind GeoLite2 City)
|
||||||
@@ -79,6 +80,11 @@ interface IBootstrapEntry {
|
|||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IRdapNetworkInfo {
|
||||||
|
networkRange: string | null;
|
||||||
|
networkCidrs: string[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IpIntelligence provides IP address intelligence by combining three data sources:
|
* IpIntelligence provides IP address intelligence by combining three data sources:
|
||||||
* - RDAP (direct to RIRs) for registration/org data
|
* - RDAP (direct to RIRs) for registration/org data
|
||||||
@@ -120,6 +126,7 @@ export class IpIntelligence {
|
|||||||
registrantOrg: null,
|
registrantOrg: null,
|
||||||
registrantCountry: null,
|
registrantCountry: null,
|
||||||
networkRange: null,
|
networkRange: null,
|
||||||
|
networkCidrs: null,
|
||||||
abuseContact: null,
|
abuseContact: null,
|
||||||
country: null,
|
country: null,
|
||||||
countryCode: null,
|
countryCode: null,
|
||||||
@@ -143,6 +150,7 @@ export class IpIntelligence {
|
|||||||
result.registrantOrg = rdap.registrantOrg;
|
result.registrantOrg = rdap.registrantOrg;
|
||||||
result.registrantCountry = rdap.registrantCountry;
|
result.registrantCountry = rdap.registrantCountry;
|
||||||
result.networkRange = rdap.networkRange;
|
result.networkRange = rdap.networkRange;
|
||||||
|
result.networkCidrs = rdap.networkCidrs;
|
||||||
result.abuseContact = rdap.abuseContact;
|
result.abuseContact = rdap.abuseContact;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,6 +265,7 @@ export class IpIntelligence {
|
|||||||
registrantOrg: string | null;
|
registrantOrg: string | null;
|
||||||
registrantCountry: string | null;
|
registrantCountry: string | null;
|
||||||
networkRange: string | null;
|
networkRange: string | null;
|
||||||
|
networkCidrs: string[] | null;
|
||||||
abuseContact: string | null;
|
abuseContact: string | null;
|
||||||
} | null> {
|
} | null> {
|
||||||
await this.ensureBootstrap();
|
await this.ensureBootstrap();
|
||||||
@@ -280,14 +289,7 @@ export class IpIntelligence {
|
|||||||
let registrantCountry: string | null = data.country || null;
|
let registrantCountry: string | null = data.country || null;
|
||||||
let abuseContact: string | null = null;
|
let abuseContact: string | null = null;
|
||||||
|
|
||||||
// Parse network range
|
const { networkRange, networkCidrs } = this.parseRdapNetworkInfo(data);
|
||||||
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}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse entities
|
// Parse entities
|
||||||
if (data.entities && Array.isArray(data.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) {
|
} catch (err: any) {
|
||||||
this.logger.debug?.(`RDAP query failed for ${ip}: ${err.message}`);
|
this.logger.debug?.(`RDAP query failed for ${ip}: ${err.message}`);
|
||||||
return null;
|
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
|
* Extract the 'fn' (formatted name) from an entity's vcardArray
|
||||||
*/
|
*/
|
||||||
@@ -573,4 +609,56 @@ export class IpIntelligence {
|
|||||||
parseInt(parts[3], 10)) >>> 0
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user