Skip to content

Commit 753ee39

Browse files
feat(radar): add BGP RPKI ASPA endpoint tools (#301)
Add three new MCP tools for Cloudflare Radar BGP RPKI ASPA endpoints: - get_bgp_rpki_aspa_snapshot: retrieve current/historical ASPA objects - get_bgp_rpki_aspa_changes: track ASPA additions, removals, modifications - get_bgp_rpki_aspa_timeseries: monitor ASPA adoption over time Also adds corresponding Zod parameter schemas for ASPA-specific fields (customer/provider ASN, RIR, change type, date, pagination, sorting). Co-authored-by: André Jesus <74548852+andre-j3sus@users.noreply.github.com>
1 parent 0fa7033 commit 753ee39

2 files changed

Lines changed: 236 additions & 0 deletions

File tree

apps/radar/src/tools/radar.tools.ts

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ import {
1919
AsnArrayParam,
2020
AsnParam,
2121
AsOrderByParam,
22+
AspaChangeTypeParam,
23+
AspaCustomerAsnParam,
24+
AspaDateParam,
25+
AspaPageParam,
26+
AspaPerPageParam,
27+
AspaProviderAsnParam,
28+
AspaRirParam,
29+
AspaSortByParam,
2230
AttackNormalizationParam,
2331
BgpHijackerAsnParam,
2432
BgpInvalidOnlyParam,
@@ -3043,4 +3051,179 @@ export function registerRadarTools(agent: RadarMCP) {
30433051
}
30443052
}
30453053
)
3054+
3055+
// ============================================================
3056+
// BGP RPKI ASPA Tools
3057+
// ============================================================
3058+
3059+
agent.server.tool(
3060+
'get_bgp_rpki_aspa_snapshot',
3061+
'Retrieve a snapshot of current or historical RPKI ASPA (Autonomous System Provider Authorization) objects. ASPA objects define which ASNs are authorized upstream providers for a customer ASN, helping prevent route leaks and hijacks.',
3062+
{
3063+
customerAsn: AspaCustomerAsnParam,
3064+
providerAsn: AspaProviderAsnParam,
3065+
rir: AspaRirParam,
3066+
location: LocationParam.optional().describe('Filter by country (alpha-2 code).'),
3067+
date: AspaDateParam,
3068+
page: AspaPageParam,
3069+
per_page: AspaPerPageParam,
3070+
sortBy: AspaSortByParam,
3071+
sortOrder: BgpSortOrderParam,
3072+
},
3073+
async ({
3074+
customerAsn,
3075+
providerAsn,
3076+
rir,
3077+
location,
3078+
date,
3079+
page,
3080+
per_page,
3081+
sortBy,
3082+
sortOrder,
3083+
}) => {
3084+
try {
3085+
const props = getProps(agent)
3086+
const result = await fetchRadarApi(props.accessToken, '/bgp/rpki/aspa/snapshot', {
3087+
customerAsn,
3088+
providerAsn,
3089+
rir,
3090+
location,
3091+
date,
3092+
page,
3093+
per_page,
3094+
sortBy,
3095+
sortOrder,
3096+
})
3097+
3098+
return {
3099+
content: [
3100+
{
3101+
type: 'text' as const,
3102+
text: JSON.stringify({ result }),
3103+
},
3104+
],
3105+
}
3106+
} catch (error) {
3107+
return {
3108+
content: [
3109+
{
3110+
type: 'text' as const,
3111+
text: `Error getting BGP RPKI ASPA snapshot: ${error instanceof Error ? error.message : String(error)}`,
3112+
},
3113+
],
3114+
}
3115+
}
3116+
}
3117+
)
3118+
3119+
agent.server.tool(
3120+
'get_bgp_rpki_aspa_changes',
3121+
'Retrieve RPKI ASPA changes over time, including additions, removals, and modifications of ASPA objects.',
3122+
{
3123+
customerAsn: AspaCustomerAsnParam,
3124+
providerAsn: AspaProviderAsnParam,
3125+
changeType: AspaChangeTypeParam,
3126+
rir: AspaRirParam,
3127+
location: LocationParam.optional().describe('Filter by country (alpha-2 code).'),
3128+
dateRange: DateRangeParam.optional(),
3129+
dateStart: DateStartParam.optional(),
3130+
dateEnd: DateEndParam.optional(),
3131+
sortBy: AspaSortByParam,
3132+
sortOrder: BgpSortOrderParam,
3133+
page: AspaPageParam,
3134+
per_page: AspaPerPageParam,
3135+
},
3136+
async ({
3137+
customerAsn,
3138+
providerAsn,
3139+
changeType,
3140+
rir,
3141+
location,
3142+
dateRange,
3143+
dateStart,
3144+
dateEnd,
3145+
sortBy,
3146+
sortOrder,
3147+
page,
3148+
per_page,
3149+
}) => {
3150+
try {
3151+
const props = getProps(agent)
3152+
const result = await fetchRadarApi(props.accessToken, '/bgp/rpki/aspa/changes', {
3153+
customerAsn,
3154+
providerAsn,
3155+
changeType,
3156+
rir,
3157+
location,
3158+
dateRange,
3159+
dateStart,
3160+
dateEnd,
3161+
sortBy,
3162+
sortOrder,
3163+
page,
3164+
per_page,
3165+
})
3166+
3167+
return {
3168+
content: [
3169+
{
3170+
type: 'text' as const,
3171+
text: JSON.stringify({ result }),
3172+
},
3173+
],
3174+
}
3175+
} catch (error) {
3176+
return {
3177+
content: [
3178+
{
3179+
type: 'text' as const,
3180+
text: `Error getting BGP RPKI ASPA changes: ${error instanceof Error ? error.message : String(error)}`,
3181+
},
3182+
],
3183+
}
3184+
}
3185+
}
3186+
)
3187+
3188+
agent.server.tool(
3189+
'get_bgp_rpki_aspa_timeseries',
3190+
'Retrieve a timeseries of RPKI ASPA object counts over time.',
3191+
{
3192+
rir: AspaRirParam,
3193+
location: LocationParam.optional().describe('Filter by country (alpha-2 code).'),
3194+
dateRange: DateRangeParam.optional(),
3195+
dateStart: DateStartParam.optional(),
3196+
dateEnd: DateEndParam.optional(),
3197+
},
3198+
async ({ rir, location, dateRange, dateStart, dateEnd }) => {
3199+
try {
3200+
const props = getProps(agent)
3201+
const result = await fetchRadarApi(props.accessToken, '/bgp/rpki/aspa/timeseries', {
3202+
rir,
3203+
location,
3204+
dateRange,
3205+
dateStart,
3206+
dateEnd,
3207+
})
3208+
3209+
return {
3210+
content: [
3211+
{
3212+
type: 'text' as const,
3213+
text: JSON.stringify({ result }),
3214+
},
3215+
],
3216+
}
3217+
} catch (error) {
3218+
return {
3219+
content: [
3220+
{
3221+
type: 'text' as const,
3222+
text: `Error getting BGP RPKI ASPA timeseries: ${error instanceof Error ? error.message : String(error)}`,
3223+
},
3224+
],
3225+
}
3226+
}
3227+
}
3228+
)
30463229
}

apps/radar/src/types/radar.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,3 +1043,56 @@ export const TrafficAnomalyStatusParam = z
10431043
.enum(['VERIFIED', 'UNVERIFIED'])
10441044
.optional()
10451045
.describe('Filter by anomaly verification status.')
1046+
1047+
// ============================================================
1048+
// BGP RPKI ASPA Parameters
1049+
// ============================================================
1050+
1051+
export const AspaCustomerAsnParam = z
1052+
.number()
1053+
.int()
1054+
.positive()
1055+
.optional()
1056+
.describe('Filter by customer ASN (the ASN that authorizes upstream providers).')
1057+
1058+
export const AspaProviderAsnParam = z
1059+
.number()
1060+
.int()
1061+
.positive()
1062+
.optional()
1063+
.describe('Filter by provider ASN (the authorized upstream provider ASN).')
1064+
1065+
export const AspaRirParam = z
1066+
.enum(['AFRINIC', 'APNIC', 'ARIN', 'LACNIC', 'RIPE_NCC'])
1067+
.optional()
1068+
.describe('Filter by Regional Internet Registry (RIR).')
1069+
1070+
export const AspaChangeTypeParam = z
1071+
.enum(['addition', 'removal', 'modification'])
1072+
.optional()
1073+
.describe('Filter by type of ASPA change.')
1074+
1075+
export const AspaSortByParam = z
1076+
.enum(['customerAsn', 'providerAsn'])
1077+
.optional()
1078+
.describe('Sort ASPA results by specified field.')
1079+
1080+
export const AspaDateParam = z
1081+
.string()
1082+
.optional()
1083+
.describe('Date for historical ASPA snapshot (ISO 8601 format, e.g. 2024-01-15).')
1084+
1085+
export const AspaPageParam = z
1086+
.number()
1087+
.int()
1088+
.positive()
1089+
.optional()
1090+
.describe('Page number for paginated ASPA results.')
1091+
1092+
export const AspaPerPageParam = z
1093+
.number()
1094+
.int()
1095+
.min(1)
1096+
.max(100)
1097+
.optional()
1098+
.describe('Number of results per page for ASPA queries (1-100).')

0 commit comments

Comments
 (0)