diff --git a/apps/radar/README.md b/apps/radar/README.md index e0999567..806aa790 100644 --- a/apps/radar/README.md +++ b/apps/radar/README.md @@ -10,40 +10,135 @@ Internet traffic insights, trends and other utilities. Currently available tools: -| **Category** | **Tool** | **Description** | -| ---------------------- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------- | -| **AI** | `get_ai_data` | Retrieves AI-related data, including traffic from AI user agents, as well as popular models and model tasks | -| **Autonomous Systems** | `list_autonomous_systems` | Lists ASes; filter by location and sort by population size | -| | `get_as_details` | Retrieves detailed info for a specific ASN | -| **Domains** | `get_domains_ranking` | Gets top or trending domains | -| | `get_domain_rank_details` | Gets domain rank details | -| **DNS** | `get_dns_data` | Retrieves DNS query data to 1.1.1.1, including timeseries, summaries, and breakdowns by dimensions like `queryType` | -| **Email Routing** | `get_email_routing_data` | Retrieves Email Routing data, including timeseries, and breakdowns by dimensions like `encrypted` | -| **Email Security** | `get_email_security_data` | Retrieves Email Security data, including timeseries, and breakdowns by dimensions like `threatCategory` | -| **HTTP** | `get_http_data` | Retrieves HTTP request data, including timeseries, and breakdowns by dimensions like `deviceType` | -| **IP Addresses** | `get_ip_details` | Provides details about a specific IP address | -| **Internet Services** | `get_internet_services_ranking` | Gets top Internet services | -| **Internet Quality** | `get_internet_quality_data` | Retrieves a summary or time series of bandwidth, latency, or DNS response time from the Radar Internet Quality Index | -| **Internet Speed** | `get_internet_speed_data` | Retrieves summary of bandwidth, latency, jitter, and packet loss, from the previous 90 days of Cloudflare Speed Test | -| **Layer 3 Attacks** | `get_l3_attack_data` | Retrieves L3 attack data, including timeseries, top attacks, and breakdowns by dimensions like `protocol` | -| **Layer 7 Attacks** | `get_l7_attack_data` | Retrieves L7 attack data, including timeseries, top attacks, and breakdowns by dimensions like `mitigationProduct` | -| **Traffic Anomalies** | `get_traffic_anomalies` | Lists traffic anomalies and outages; filter by AS, location, start date, and end date | -| **URL Scanner** | `scan_url` | Scans a URL via [Cloudflare’s URL Scanner](https://developers.cloudflare.com/radar/investigate/url-scanner/) | - -This MCP server is still a work in progress, and we plan to add more tools in the future. +| **Category** | **Tool** | **Description** | +| ---------------------------- | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------- | +| **AI** | `get_ai_data` | Retrieves AI-related data, including traffic from AI user agents, as well as popular models and model tasks | +| **Annotations & Outages** | `get_annotations` | Retrieves annotations including Internet events, outages, and anomalies from various Cloudflare data sources | +| | `get_outages` | Retrieves Internet outages and anomalies with detected connectivity issues | +| **AS112** | `get_as112_data` | Retrieves AS112 DNS sink hole data for reverse DNS lookups of private IP addresses (RFC 1918) | +| **Autonomous Systems** | `list_autonomous_systems` | Lists ASes; filter by location and sort by population size | +| | `get_as_details` | Retrieves detailed info for a specific ASN | +| | `get_as_set` | Gets IRR AS-SETs that an AS is a member of | +| | `get_as_relationships` | Gets AS-level relationships (peer, upstream, downstream) | +| **BGP** | `get_bgp_hijacks` | Retrieves BGP hijack events with filtering by hijacker/victim ASN, confidence score | +| | `get_bgp_leaks` | Retrieves BGP route leak events | +| | `get_bgp_route_stats` | Retrieves BGP routing table statistics | +| | `get_bgp_timeseries` | Retrieves BGP updates time series (announcements and withdrawals) | +| | `get_bgp_top_ases` | Gets top ASes by BGP update count | +| | `get_bgp_top_prefixes` | Gets top IP prefixes by BGP update count | +| | `get_bgp_moas` | Gets Multi-Origin AS (MOAS) prefixes | +| | `get_bgp_pfx2as` | Gets prefix-to-ASN mapping | +| **Bots** | `get_bots_data` | Retrieves bot traffic data by name, operator, category (AI crawlers, search engines, etc.) | +| | `list_bots` | Lists known bots with details (AI crawlers, search engines, monitoring bots) | +| | `get_bot_details` | Gets detailed information about a specific bot by slug | +| | `get_bots_crawlers_data` | Retrieves web crawler HTTP request data by client type, user agent, referrer, industry | +| **Certificate Transparency** | `get_certificate_transparency_data` | Retrieves CT log data for SSL/TLS certificate issuance trends | +| | `list_ct_authorities` | Lists Certificate Authorities tracked in CT logs | +| | `get_ct_authority_details` | Gets details for a specific CA by fingerprint | +| | `list_ct_logs` | Lists Certificate Transparency logs | +| | `get_ct_log_details` | Gets details for a specific CT log by slug | +| **Cloud Observatory** | `list_origins` | Lists cloud provider origins (AWS, GCP, Azure, OCI) | +| | `get_origin_details` | Gets details for a specific cloud provider | +| | `get_origins_data` | Retrieves cloud provider performance metrics (timeseries, summaries, grouped by region/percentile) | +| **Domains** | `get_domains_ranking` | Gets top or trending domains | +| | `get_domain_rank_details` | Gets domain rank details | +| **DNS** | `get_dns_queries_data` | Retrieves DNS query data to 1.1.1.1, including timeseries, summaries, and breakdowns by dimensions like `queryType` | +| **Email Routing** | `get_email_routing_data` | Retrieves Email Routing data, including timeseries, and breakdowns by dimensions like `encrypted` | +| **Email Security** | `get_email_security_data` | Retrieves Email Security data, including timeseries, and breakdowns by dimensions like `threatCategory` | +| **Geolocations** | `list_geolocations` | Lists available geolocations (ADM1 - states/provinces) with GeoNames IDs | +| | `get_geolocation_details` | Gets details for a specific geolocation by GeoNames ID | +| **HTTP** | `get_http_data` | Retrieves HTTP request data with geoId filtering for ADM1 (states/provinces) | +| **IP Addresses** | `get_ip_details` | Provides details about a specific IP address | +| **Internet Services** | `get_internet_services_ranking` | Gets top Internet services | +| **Internet Quality** | `get_internet_quality_data` | Retrieves a summary or time series of bandwidth, latency, or DNS response time from the Radar Internet Quality Index | +| **Internet Speed** | `get_internet_speed_data` | Retrieves summary of bandwidth, latency, jitter, and packet loss, from the previous 90 days of Cloudflare Speed Test | +| **Layer 3 Attacks** | `get_l3_attack_data` | Retrieves L3 attack data, including timeseries, top attacks, and breakdowns by dimensions like `protocol` | +| **Layer 7 Attacks** | `get_l7_attack_data` | Retrieves L7 attack data, including timeseries, top attacks, and breakdowns by dimensions like `mitigationProduct` | +| **Leaked Credentials** | `get_leaked_credentials_data` | Retrieves trends in HTTP auth requests and compromised credential detection | +| **NetFlows** | `get_netflows_data` | Retrieves network traffic patterns with geoId filtering for ADM1 (states/provinces) | +| **Robots.txt** | `get_robots_txt_data` | Retrieves robots.txt analysis data showing crawler access rules across domains | +| **TCP Quality** | `get_tcp_resets_timeouts_data` | Retrieves TCP connection quality metrics (resets and timeouts) | +| **Traffic Anomalies** | `get_traffic_anomalies` | Lists traffic anomalies and outages; filter by AS, location, start date, and end date | +| **URL Scanner** | `search_url_scans` | Search URL scans using ElasticSearch-like query syntax | +| | `create_url_scan` | Submit a URL to scan, returns scan UUID | +| | `get_url_scan` | Get scan results by UUID (verdicts, page info, stats) | +| | `get_url_scan_screenshot` | Get screenshot URL for a completed scan | +| | `get_url_scan_har` | Get HAR (HTTP Archive) data for a completed scan | ### Prompt Examples +**Traffic & Network Analysis** + - `What are the most used operating systems?` +- `Show me HTTP traffic trends from Lisbon, Portugal (use geoId).` +- `What is the TCP reset and timeout rate globally?` +- `Show me network traffic patterns for California.` + +**Autonomous Systems & BGP** + - `What are the top 5 ASes in Portugal?` - `Get information about ASN 13335.` -- `What are the details of IP address 1.1.1.1?` -- `List me traffic anomalies in Syria over the last year.` +- `What are the relationships (peers, upstreams) for Cloudflare's AS?` +- `Show me recent BGP hijack events.` +- `Which prefixes have the most BGP updates?` +- `What AS announces the prefix 1.1.1.0/24?` + +**Security & Attacks** + +- `Show me application layer attack trends from the last 7 days.` +- `What are the top L3 attack vectors?` +- `Show me leaked credential detection trends.` +- `Scan https://example.com for security analysis.` + +**Bots & Crawlers** + +- `What AI crawlers are most active?` +- `List all known AI crawler bots.` +- `How are websites configuring robots.txt for AI crawlers?` +- `What percentage of sites block vs allow AI crawlers?` +- `Show me crawler traffic by industry vertical.` + +**DNS & Email** + +- `What are the most common DNS query types to 1.1.1.1?` +- `Show me AS112 DNS sink hole data by protocol.` +- `What are the email security threat trends?` + +**Certificates & TLS** + +- `What are the most active Certificate Authorities?` +- `List Certificate Transparency logs.` +- `Show me certificate issuance trends by validation level.` + +**Rankings & Services** + +- `What are the top trending domains?` - `Compare domain rankings in the US and UK.` - `Give me rank details for google.com in March 2025.` -- `Scan https://example.com.` -- `Show me HTTP traffic trends from Portugal.` -- `Show me application layer attack trends from the last 7 days.` +- `What are the top Internet services in the E-commerce category?` + +**Outages & Events** + +- `List me traffic anomalies in Syria over the last year.` +- `Show me recent Internet outages.` +- `What outages affected Portugal in the last 30 days?` + +**Cloud & Infrastructure** + +- `What are the top 5 AWS regions in terms of traffic?` +- `Compare latency between Azure and GCP regions.` +- `What is the connection success rate for cloud providers?` + +**Geolocations** + +- `List available geolocations for Portugal.` +- `What is the GeoNames ID for Lisbon?` +- `Show me HTTP traffic specifically for the Lisbon area.` + +**IP Information** + +- `What are the details of IP address 1.1.1.1?` +- `What ASN owns this IP address?` ## Access the remote MCP server from any MCP Client diff --git a/apps/radar/src/radar.context.ts b/apps/radar/src/radar.context.ts index f1bb3ede..66fa2172 100644 --- a/apps/radar/src/radar.context.ts +++ b/apps/radar/src/radar.context.ts @@ -19,24 +19,48 @@ export interface Env { export const BASE_INSTRUCTIONS = /* markdown */ ` # Cloudflare Radar MCP Server -This server integrates tools powered by the Cloudflare Radar API to provide insights into global Internet traffic, -trends, and other related utilities. +This server provides tools powered by the Cloudflare Radar API for global Internet insights. -An active account is **only required** for URL Scanner-related tools (e.g., \`scan_url\`). +## Authentication -For tools related to Internet trends and insights, analyze the results and, when appropriate, generate visualizations -such as line charts, pie charts, bar charts, stacked area charts, choropleth maps, treemaps, or other relevant chart types. +- **URL Scanner** requires an active account (use \`set_active_account\`) +- All other Radar data tools work without account selection -### Making comparisons +## Tool Categories -Many tools support **array-based filters** to enable comparisons across multiple criteria. -In such cases, the array index corresponds to a distinct data series. -For each data series, provide a corresponding \`dateRange\`, or alternatively a \`dateStart\` and \`dateEnd\` pair. -Example: To compare HTTP traffic between Portugal and Spain over the last 7 days: +- **Entities**: Look up ASN, IP, and location details (\`list_autonomous_systems\`, \`get_as_details\`, \`get_ip_details\`) +- **Traffic**: HTTP and DNS trends (\`get_http_data\`, \`get_dns_queries_data\`) +- **Attacks**: Layer 3/7 DDoS attack trends (\`get_l3_attack_data\`, \`get_l7_attack_data\`) +- **Email**: Routing and security trends (\`get_email_routing_data\`, \`get_email_security_data\`) +- **Quality**: Internet speed and quality metrics (\`get_internet_quality_data\`, \`get_internet_speed_data\`) +- **Rankings**: Top domains and services (\`get_domains_ranking\`, \`get_internet_services_ranking\`) +- **AI**: AI bot traffic and Workers AI usage (\`get_ai_data\`) +- **BGP**: Route hijacks, leaks, and stats (\`get_bgp_hijacks\`, \`get_bgp_leaks\`, \`get_bgp_route_stats\`) +- **Bots**: Bot traffic by category, operator, kind (\`get_bots_data\`) +- **Certificate Transparency**: SSL/TLS certificate issuance trends (\`get_certificate_transparency_data\`) +- **NetFlows**: Network traffic patterns with ADM1 filtering (\`get_netflows_data\`) +- **Cloud Observatory**: Cloud provider performance - AWS, GCP, Azure, OCI (\`list_origins\`, \`get_origin_details\`, \`get_origins_data\`) +- **URL Scanner**: Scan and analyze URLs for security threats (\`search_url_scans\`, \`create_url_scan\`, \`get_url_scan\`, \`get_url_scan_screenshot\`, \`get_url_scan_har\`) + +## Making Comparisons + +Many tools support **array-based filters** for comparisons. Each array index corresponds to a distinct data series. +Example: Compare HTTP traffic between Portugal and Spain over the last 7 days: - \`dateRange: ["7d", "7d"]\` - \`location: ["PT", "ES"]\` -This applies to date filters and other filters that support comparison across multiple values. -If a tool does **not** support array-based filters, you can achieve the same comparison by making multiple separate -calls to the tool. +## Geographic Filtering + +- **location**: Filter by country (alpha-2 codes like "US", "PT") +- **continent**: Filter by continent (alpha-2 codes like "EU", "NA") +- **geoId**: Filter by ADM1 region (GeoNames IDs for states/provinces) - available for HTTP and NetFlows + +## Visualizations + +Generate charts when appropriate: +- **Line charts**: Timeseries data +- **Bar charts**: Rankings, summaries +- **Pie charts**: Distributions +- **Choropleth maps**: Geographic data +- **Stacked area charts**: Grouped timeseries ` diff --git a/apps/radar/src/tools/radar.tools.ts b/apps/radar/src/tools/radar.tools.ts index cf7ec850..67aff44f 100644 --- a/apps/radar/src/tools/radar.tools.ts +++ b/apps/radar/src/tools/radar.tools.ts @@ -9,10 +9,51 @@ import { import { AiDimensionParam, + AnnotationDataSourceParam, + AnnotationEventTypeParam, + As112DimensionParam, + As112ProtocolParam, + As112QueryTypeParam, + As112ResponseCodeParam, AsnArrayParam, AsnParam, AsOrderByParam, + BgpHijackerAsnParam, + BgpInvalidOnlyParam, + BgpInvolvedAsnParam, + BgpInvolvedCountryParam, + BgpLeakAsnParam, + BgpLongestPrefixMatchParam, + BgpMaxConfidenceParam, + BgpMinConfidenceParam, + BgpOriginParam, + BgpPrefixArrayParam, + BgpPrefixParam, + BgpRpkiStatusParam, + BgpSortByParam, + BgpSortOrderParam, + BgpUpdateTypeParam, + BgpVictimAsnParam, + BotCategoryParam, + BotKindParam, + BotNameParam, + BotOperatorParam, + BotsCrawlersDimensionParam, + BotsCrawlersFormatParam, + BotsDimensionParam, + BotVerificationStatusParam, ContinentArrayParam, + CrawlerClientTypeParam, + CrawlerIndustryParam, + CrawlerVerticalParam, + CtCaOwnerParam, + CtCaParam, + CtDimensionParam, + CtDurationParam, + CtEntryTypeParam, + CtPublicKeyAlgorithmParam, + CtTldParam, + CtValidationLevelParam, DateEndArrayParam, DateEndParam, DateListParam, @@ -25,6 +66,8 @@ import { DomainRankingTypeParam, EmailRoutingDimensionParam, EmailSecurityDimensionParam, + GeoIdArrayParam, + GeoIdParam, HttpDimensionParam, InternetQualityMetricParam, InternetServicesCategoryParam, @@ -33,14 +76,80 @@ import { IpParam, L3AttackDimensionParam, L7AttackDimensionParam, + LeakedCredentialsBotClassParam, + LeakedCredentialsCompromisedParam, + LeakedCredentialsDimensionParam, + LimitPerGroupParam, LocationArrayParam, LocationListParam, LocationParam, + NetflowsDimensionParam, + NetflowsProductParam, + NormalizationParam, + OriginArrayParam, + OriginDataDimensionParam, + OriginMetricParam, + OriginNormalizationParam, + OriginRegionParam, + OriginSlugParam, + RobotsTxtDimensionParam, + RobotsTxtDirectiveParam, + RobotsTxtDomainCategoryParam, + RobotsTxtPatternParam, + RobotsTxtUserAgentCategoryParam, + TcpResetsTimeoutsDimensionParam, } from '../types/radar' import { resolveAndInvoke } from '../utils' import type { RadarMCP } from '../radar.app' +const RADAR_API_BASE = 'https://api.cloudflare.com/client/v4/radar' + +/** + * Helper function to make authenticated requests to the Radar API + * Used for endpoints not yet available in the Cloudflare SDK + */ +async function fetchRadarApi( + accessToken: string, + endpoint: string, + params: Record = {} +): Promise { + const url = new URL(`${RADAR_API_BASE}${endpoint}`) + + for (const [key, value] of Object.entries(params)) { + if (value === undefined || value === null) continue + + if (Array.isArray(value)) { + for (const item of value) { + url.searchParams.append(key, String(item)) + } + } else { + url.searchParams.set(key, String(value)) + } + } + + const response = await fetch(url.toString(), { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }) + + if (!response.ok) { + const errorBody = await response.text() + throw new Error(`API request failed (${response.status}): ${errorBody}`) + } + + const data = (await response.json()) as { success: boolean; result: unknown; errors?: unknown[] } + + if (!data.success) { + throw new Error(`API returned error: ${JSON.stringify(data.errors)}`) + } + + return data.result +} + export function registerRadarTools(agent: RadarMCP) { agent.server.tool( 'list_autonomous_systems', @@ -335,9 +444,10 @@ export function registerRadarTools(agent: RadarMCP) { asn: AsnArrayParam, continent: ContinentArrayParam, location: LocationArrayParam, + geoId: GeoIdArrayParam, dimension: HttpDimensionParam, }, - async ({ dateStart, dateEnd, dateRange, asn, location, continent, dimension }) => { + async ({ dateStart, dateEnd, dateRange, asn, location, continent, geoId, dimension }) => { try { const props = getProps(agent) const client = getCloudflareClient(props.accessToken) @@ -345,6 +455,7 @@ export function registerRadarTools(agent: RadarMCP) { asn, continent, location, + geoId, dateRange, dateStart, dateEnd, @@ -747,4 +858,1619 @@ export function registerRadarTools(agent: RadarMCP) { } } ) + + // ============================================================ + // BGP Tools + // TODO: Replace with SDK when BGP hijacks/leaks endpoints work correctly in cloudflare SDK + // ============================================================ + + agent.server.tool( + 'get_bgp_hijacks', + 'Retrieve BGP hijack events. BGP hijacks occur when an AS announces routes it does not own, potentially redirecting traffic.', + { + limit: PaginationLimitParam, + offset: PaginationOffsetParam, + dateRange: DateRangeParam.optional(), + dateStart: DateStartParam.optional(), + dateEnd: DateEndParam.optional(), + hijackerAsn: BgpHijackerAsnParam, + victimAsn: BgpVictimAsnParam, + involvedAsn: BgpInvolvedAsnParam, + involvedCountry: BgpInvolvedCountryParam, + prefix: BgpPrefixParam, + minConfidence: BgpMinConfidenceParam, + maxConfidence: BgpMaxConfidenceParam, + sortBy: BgpSortByParam, + sortOrder: BgpSortOrderParam, + }, + async ({ + limit, + offset, + dateRange, + dateStart, + dateEnd, + hijackerAsn, + victimAsn, + involvedAsn, + involvedCountry, + prefix, + minConfidence, + maxConfidence, + sortBy, + sortOrder, + }) => { + try { + const props = getProps(agent) + const result = await fetchRadarApi(props.accessToken, '/bgp/hijacks/events', { + page: offset ? Math.floor(offset / (limit || 10)) + 1 : 1, + per_page: limit, + dateRange, + dateStart, + dateEnd, + hijackerAsn, + victimAsn, + involvedAsn, + involvedCountry, + prefix, + minConfidence, + maxConfidence, + sortBy, + sortOrder, + }) + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ result }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error getting BGP hijacks: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + agent.server.tool( + 'get_bgp_leaks', + 'Retrieve BGP route leak events. Route leaks occur when an AS improperly announces routes learned from one peer to another.', + { + limit: PaginationLimitParam, + offset: PaginationOffsetParam, + dateRange: DateRangeParam.optional(), + dateStart: DateStartParam.optional(), + dateEnd: DateEndParam.optional(), + leakAsn: BgpLeakAsnParam, + involvedAsn: BgpInvolvedAsnParam, + involvedCountry: BgpInvolvedCountryParam, + sortBy: BgpSortByParam, + sortOrder: BgpSortOrderParam, + }, + async ({ + limit, + offset, + dateRange, + dateStart, + dateEnd, + leakAsn, + involvedAsn, + involvedCountry, + sortBy, + sortOrder, + }) => { + try { + const props = getProps(agent) + const result = await fetchRadarApi(props.accessToken, '/bgp/leaks/events', { + page: offset ? Math.floor(offset / (limit || 10)) + 1 : 1, + per_page: limit, + dateRange, + dateStart, + dateEnd, + leakAsn, + involvedAsn, + involvedCountry, + sortBy, + sortOrder, + }) + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ result }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error getting BGP leaks: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + agent.server.tool( + 'get_bgp_route_stats', + 'Retrieve BGP routing table statistics including number of routes, origin ASes, and more.', + { + asn: AsnParam.optional(), + location: LocationParam.optional(), + }, + async ({ asn, location }) => { + try { + const props = getProps(agent) + const client = getCloudflareClient(props.accessToken) + const r = await client.radar.bgp.routes.stats({ + asn, + location, + }) + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ result: r }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error getting BGP route stats: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + // ============================================================ + // Bots Tools + // TODO: Replace with SDK when bots endpoints are added to cloudflare SDK + // ============================================================ + + agent.server.tool( + 'get_bots_data', + 'Retrieve bot traffic data including trends by bot name, operator, category, and kind. Covers AI crawlers, search engines, monitoring bots, and more.', + { + dateRange: DateRangeArrayParam.optional(), + dateStart: DateStartArrayParam.optional(), + dateEnd: DateEndArrayParam.optional(), + asn: AsnArrayParam, + continent: ContinentArrayParam, + location: LocationArrayParam, + bot: BotNameParam, + botOperator: BotOperatorParam, + botCategory: BotCategoryParam, + botKind: BotKindParam, + botVerificationStatus: BotVerificationStatusParam, + dimension: BotsDimensionParam, + limitPerGroup: LimitPerGroupParam, + }, + async ({ + dateRange, + dateStart, + dateEnd, + asn, + continent, + location, + bot, + botOperator, + botCategory, + botKind, + botVerificationStatus, + dimension, + limitPerGroup, + }) => { + try { + const props = getProps(agent) + + const endpoint = dimension === 'timeseries' ? '/bots/timeseries' : `/bots/${dimension}` + + const result = await fetchRadarApi(props.accessToken, endpoint, { + asn, + continent, + location, + dateRange, + dateStart, + dateEnd, + bot, + botOperator, + botCategory, + botKind, + botVerificationStatus, + limitPerGroup: dimension !== 'timeseries' ? limitPerGroup : undefined, + }) + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ result }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error getting bots data: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + // ============================================================ + // Certificate Transparency Tools + // TODO: Replace with SDK when CT endpoints are added to cloudflare SDK + // ============================================================ + + agent.server.tool( + 'get_certificate_transparency_data', + 'Retrieve Certificate Transparency (CT) log data. CT provides visibility into SSL/TLS certificates issued for domains, useful for security monitoring.', + { + dateRange: DateRangeArrayParam.optional(), + dateStart: DateStartArrayParam.optional(), + dateEnd: DateEndArrayParam.optional(), + ca: CtCaParam, + caOwner: CtCaOwnerParam, + duration: CtDurationParam, + entryType: CtEntryTypeParam, + tld: CtTldParam, + validationLevel: CtValidationLevelParam, + publicKeyAlgorithm: CtPublicKeyAlgorithmParam, + dimension: CtDimensionParam, + limitPerGroup: LimitPerGroupParam, + }, + async ({ + dateRange, + dateStart, + dateEnd, + ca, + caOwner, + duration, + entryType, + tld, + validationLevel, + publicKeyAlgorithm, + dimension, + limitPerGroup, + }) => { + try { + const props = getProps(agent) + + const endpoint = dimension === 'timeseries' ? '/ct/timeseries' : `/ct/${dimension}` + + const result = await fetchRadarApi(props.accessToken, endpoint, { + dateRange, + dateStart, + dateEnd, + ca, + caOwner, + duration, + entryType, + tld, + validationLevel, + publicKeyAlgorithm, + limitPerGroup: dimension !== 'timeseries' ? limitPerGroup : undefined, + }) + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ result }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error getting CT data: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + // ============================================================ + // NetFlows Tools + // TODO: Replace with SDK when netflows endpoints support geoId in cloudflare SDK + // ============================================================ + + agent.server.tool( + 'get_netflows_data', + 'Retrieve NetFlows traffic data showing network traffic patterns. Supports filtering by ADM1 (administrative level 1, e.g., states/provinces) via geoId.', + { + dateRange: DateRangeArrayParam.optional(), + dateStart: DateStartArrayParam.optional(), + dateEnd: DateEndArrayParam.optional(), + asn: AsnArrayParam, + continent: ContinentArrayParam, + location: LocationArrayParam, + geoId: GeoIdArrayParam, + product: NetflowsProductParam, + normalization: NormalizationParam, + dimension: NetflowsDimensionParam, + limitPerGroup: LimitPerGroupParam, + }, + async ({ + dateRange, + dateStart, + dateEnd, + asn, + continent, + location, + geoId, + product, + normalization, + dimension, + limitPerGroup, + }) => { + try { + const props = getProps(agent) + + let endpoint: string + if (dimension === 'timeseries') { + endpoint = '/netflows/timeseries' + } else if (dimension === 'summary') { + endpoint = '/netflows/summary' + } else { + endpoint = `/netflows/${dimension}` + } + + const result = await fetchRadarApi(props.accessToken, endpoint, { + asn, + continent, + location, + geoId, + dateRange, + dateStart, + dateEnd, + product, + normalization, + limitPerGroup: !['timeseries', 'summary'].includes(dimension) ? limitPerGroup : undefined, + }) + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ result }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error getting NetFlows data: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + // ============================================================ + // Cloud Observatory / Origins Tools + // TODO: Replace with SDK when origins endpoints are added to cloudflare SDK + // ============================================================ + + agent.server.tool( + 'list_origins', + 'List cloud provider origins (hyperscalers) available in Cloud Observatory. Returns Amazon (AWS), Google (GCP), Microsoft (Azure), and Oracle (OCI) with their available regions.', + { + limit: PaginationLimitParam, + offset: PaginationOffsetParam, + }, + async ({ limit, offset }) => { + try { + const props = getProps(agent) + const result = await fetchRadarApi(props.accessToken, '/origins', { + limit, + offset, + }) + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ result }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error listing origins: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + agent.server.tool( + 'get_origin_details', + 'Get details for a specific cloud provider origin, including all available regions.', + { + slug: OriginSlugParam, + }, + async ({ slug }) => { + try { + const props = getProps(agent) + const result = await fetchRadarApi(props.accessToken, `/origins/${slug}`) + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ result }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error getting origin details: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + agent.server.tool( + 'get_origins_data', + 'Retrieve cloud provider (AWS, GCP, Azure, OCI) performance metrics. Supports timeseries, summaries grouped by region/success_rate/percentile, and grouped timeseries.', + { + dimension: OriginDataDimensionParam, + origin: OriginArrayParam, + metric: OriginMetricParam, + dateRange: DateRangeArrayParam.optional(), + dateStart: DateStartArrayParam.optional(), + dateEnd: DateEndArrayParam.optional(), + region: OriginRegionParam, + limitPerGroup: LimitPerGroupParam, + normalization: OriginNormalizationParam, + }, + async ({ + dimension, + origin, + metric, + dateRange, + dateStart, + dateEnd, + region, + limitPerGroup, + normalization, + }) => { + try { + const props = getProps(agent) + + let endpoint: string + if (dimension === 'timeseries') { + endpoint = '/origins/timeseries' + } else if (dimension.startsWith('summary/')) { + const groupBy = dimension.replace('summary/', '') + endpoint = `/origins/summary/${groupBy}` + } else { + const groupBy = dimension.replace('timeseriesGroups/', '') + endpoint = `/origins/timeseries_groups/${groupBy}` + } + + const result = await fetchRadarApi(props.accessToken, endpoint, { + origin, + metric, + dateRange, + dateStart, + dateEnd, + region, + limitPerGroup: dimension !== 'timeseries' ? limitPerGroup : undefined, + normalization: dimension.startsWith('timeseriesGroups/') ? normalization : undefined, + }) + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ result }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text' as const, + text: `Error getting origins data: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + // ============================================================ + // Robots.txt Tools + // ============================================================ + + agent.server.tool( + 'get_robots_txt_data', + 'Retrieve robots.txt analysis data. Shows how websites configure crawler access rules, particularly for AI crawlers. Useful for understanding web crawler policies across domains.', + { + dateRange: DateRangeArrayParam.optional(), + dateStart: DateStartArrayParam.optional(), + dateEnd: DateEndArrayParam.optional(), + date: DateListParam.optional(), + directive: RobotsTxtDirectiveParam, + pattern: RobotsTxtPatternParam, + domainCategory: RobotsTxtDomainCategoryParam, + userAgentCategory: RobotsTxtUserAgentCategoryParam, + dimension: RobotsTxtDimensionParam, + limitPerGroup: LimitPerGroupParam, + limit: PaginationLimitParam, + }, + async ({ + dateRange, + dateStart, + dateEnd, + date, + directive, + pattern, + domainCategory, + userAgentCategory, + dimension, + limitPerGroup, + limit, + }) => { + try { + const props = getProps(agent) + + const endpoint = `/robots_txt/${dimension}` + + const result = await fetchRadarApi(props.accessToken, endpoint, { + dateRange, + dateStart, + dateEnd, + date, + directive, + pattern, + domainCategory, + userAgentCategory, + limitPerGroup, + limit, + }) + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ result }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text' as const, + text: `Error getting robots.txt data: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + // ============================================================ + // Bots Crawlers Tools + // ============================================================ + + agent.server.tool( + 'get_bots_crawlers_data', + 'Retrieve web crawler HTTP request data. Shows crawler traffic patterns by client type, user agent, referrer, and industry. Useful for analyzing crawler behavior and traffic distribution.', + { + dateRange: DateRangeArrayParam.optional(), + dateStart: DateStartArrayParam.optional(), + dateEnd: DateEndArrayParam.optional(), + dimension: BotsCrawlersDimensionParam, + format: BotsCrawlersFormatParam, + botOperator: BotOperatorParam, + vertical: CrawlerVerticalParam, + industry: CrawlerIndustryParam, + clientType: CrawlerClientTypeParam, + limitPerGroup: LimitPerGroupParam, + }, + async ({ + dateRange, + dateStart, + dateEnd, + dimension, + format, + botOperator, + vertical, + industry, + clientType, + limitPerGroup, + }) => { + try { + const props = getProps(agent) + + const endpoint = `/bots/crawlers/${format}/${dimension}` + + const result = await fetchRadarApi(props.accessToken, endpoint, { + dateRange, + dateStart, + dateEnd, + botOperator, + vertical, + industry, + clientType, + limitPerGroup, + }) + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ result }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text' as const, + text: `Error getting bots crawlers data: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + agent.server.tool( + 'list_bots', + 'List known bots with their details. Includes AI crawlers, search engines, monitoring bots, and more. Filter by category, operator, kind, or verification status.', + { + limit: PaginationLimitParam, + offset: PaginationOffsetParam, + botCategory: z + .enum([ + 'SEARCH_ENGINE_CRAWLER', + 'SEARCH_ENGINE_OPTIMIZATION', + 'MONITORING_AND_ANALYTICS', + 'ADVERTISING_AND_MARKETING', + 'SOCIAL_MEDIA_MARKETING', + 'PAGE_PREVIEW', + 'ACADEMIC_RESEARCH', + 'SECURITY', + 'ACCESSIBILITY', + 'WEBHOOKS', + 'FEED_FETCHER', + 'AI_CRAWLER', + 'AGGREGATOR', + 'AI_ASSISTANT', + 'AI_SEARCH', + 'ARCHIVER', + ]) + .optional() + .describe('Filter by bot category.'), + botOperator: z.string().optional().describe('Filter by bot operator name.'), + kind: z.enum(['AGENT', 'BOT']).optional().describe('Filter by bot kind.'), + botVerificationStatus: z + .enum(['VERIFIED']) + .optional() + .describe('Filter by verification status.'), + }, + async ({ limit, offset, botCategory, botOperator, kind, botVerificationStatus }) => { + try { + const props = getProps(agent) + const result = await fetchRadarApi(props.accessToken, '/bots', { + limit, + offset, + botCategory, + botOperator, + kind, + botVerificationStatus, + }) + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ result }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text' as const, + text: `Error listing bots: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + agent.server.tool( + 'get_bot_details', + 'Get detailed information about a specific bot by its slug identifier.', + { + botSlug: z.string().describe('The bot slug identifier (e.g., "googlebot", "bingbot").'), + }, + async ({ botSlug }) => { + try { + const props = getProps(agent) + const result = await fetchRadarApi(props.accessToken, `/bots/${botSlug}`) + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ result }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text' as const, + text: `Error getting bot details: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + // ============================================================ + // Leaked Credential Checks Tools + // ============================================================ + + agent.server.tool( + 'get_leaked_credentials_data', + 'Retrieve trends in HTTP authentication requests and compromised credential detection. Shows distribution by compromised status and bot class.', + { + dateRange: DateRangeArrayParam.optional(), + dateStart: DateStartArrayParam.optional(), + dateEnd: DateEndArrayParam.optional(), + asn: AsnArrayParam, + continent: ContinentArrayParam, + location: LocationArrayParam, + botClass: LeakedCredentialsBotClassParam, + compromised: LeakedCredentialsCompromisedParam, + dimension: LeakedCredentialsDimensionParam, + }, + async ({ + dateRange, + dateStart, + dateEnd, + asn, + continent, + location, + botClass, + compromised, + dimension, + }) => { + try { + const props = getProps(agent) + + let endpoint: string + if (dimension === 'timeseries') { + endpoint = '/leaked_credential_checks/timeseries' + } else { + endpoint = `/leaked_credential_checks/${dimension}` + } + + const result = await fetchRadarApi(props.accessToken, endpoint, { + dateRange, + dateStart, + dateEnd, + asn, + continent, + location, + botClass, + compromised, + }) + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ result }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text' as const, + text: `Error getting leaked credentials data: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + // ============================================================ + // AS112 Tools + // ============================================================ + + agent.server.tool( + 'get_as112_data', + 'Retrieve AS112 DNS sink hole data. AS112 handles reverse DNS lookups for private IP addresses (RFC 1918). Useful for analyzing DNS misconfiguration patterns.', + { + dateRange: DateRangeArrayParam.optional(), + dateStart: DateStartArrayParam.optional(), + dateEnd: DateEndArrayParam.optional(), + continent: ContinentArrayParam, + location: LocationArrayParam, + queryType: As112QueryTypeParam, + protocol: As112ProtocolParam, + responseCode: As112ResponseCodeParam, + dimension: As112DimensionParam, + }, + async ({ + dateRange, + dateStart, + dateEnd, + continent, + location, + queryType, + protocol, + responseCode, + dimension, + }) => { + try { + const props = getProps(agent) + + let endpoint: string + if (dimension === 'timeseries') { + endpoint = '/as112/timeseries' + } else if (dimension === 'top/locations') { + endpoint = '/as112/top/locations' + } else { + endpoint = `/as112/${dimension}` + } + + const result = await fetchRadarApi(props.accessToken, endpoint, { + dateRange, + dateStart, + dateEnd, + continent, + location, + queryType, + protocol, + responseCode, + }) + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ result }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text' as const, + text: `Error getting AS112 data: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + // ============================================================ + // Geolocation Tools + // ============================================================ + + agent.server.tool( + 'list_geolocations', + 'List available geolocations (ADM1 - administrative divisions like states/provinces). Use this to find GeoNames IDs for filtering HTTP and NetFlows data by region.', + { + limit: PaginationLimitParam, + offset: PaginationOffsetParam, + geoId: z.string().optional().describe('Filter by specific GeoNames ID.'), + location: LocationParam.optional(), + }, + async ({ limit, offset, geoId, location }) => { + try { + const props = getProps(agent) + const result = await fetchRadarApi(props.accessToken, '/geolocations', { + limit, + offset, + geoId, + location, + }) + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ result }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text' as const, + text: `Error listing geolocations: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + agent.server.tool( + 'get_geolocation_details', + 'Get details for a specific geolocation by its GeoNames ID.', + { + geoId: GeoIdParam, + }, + async ({ geoId }) => { + try { + const props = getProps(agent) + const result = await fetchRadarApi(props.accessToken, `/geolocations/${geoId}`) + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ result }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text' as const, + text: `Error getting geolocation details: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + // ============================================================ + // TCP Resets/Timeouts Tools + // ============================================================ + + agent.server.tool( + 'get_tcp_resets_timeouts_data', + 'Retrieve TCP connection quality metrics including resets and timeouts. Useful for understanding connection reliability across networks and locations.', + { + dateRange: DateRangeArrayParam.optional(), + dateStart: DateStartArrayParam.optional(), + dateEnd: DateEndArrayParam.optional(), + asn: AsnArrayParam, + continent: ContinentArrayParam, + location: LocationArrayParam, + dimension: TcpResetsTimeoutsDimensionParam, + }, + async ({ dateRange, dateStart, dateEnd, asn, continent, location, dimension }) => { + try { + const props = getProps(agent) + + const endpoint = + dimension === 'summary' + ? '/tcp_resets_timeouts/summary' + : '/tcp_resets_timeouts/timeseries_groups' + + const result = await fetchRadarApi(props.accessToken, endpoint, { + dateRange, + dateStart, + dateEnd, + asn, + continent, + location, + }) + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ result }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text' as const, + text: `Error getting TCP resets/timeouts data: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + // ============================================================ + // Annotations/Outages Tools + // ============================================================ + + agent.server.tool( + 'get_annotations', + 'Retrieve annotations including Internet events, outages, and anomalies from various Cloudflare data sources.', + { + limit: PaginationLimitParam, + offset: PaginationOffsetParam, + dateRange: DateRangeParam.optional(), + dateStart: DateStartParam.optional(), + dateEnd: DateEndParam.optional(), + dataSource: AnnotationDataSourceParam, + eventType: AnnotationEventTypeParam, + asn: AsnParam.optional(), + location: LocationParam.optional(), + }, + async ({ + limit, + offset, + dateRange, + dateStart, + dateEnd, + dataSource, + eventType, + asn, + location, + }) => { + try { + const props = getProps(agent) + const result = await fetchRadarApi(props.accessToken, '/annotations', { + limit, + offset, + dateRange, + dateStart, + dateEnd, + dataSource, + eventType, + asn, + location, + }) + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ result }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text' as const, + text: `Error getting annotations: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + agent.server.tool( + 'get_outages', + 'Retrieve Internet outages and anomalies. Provides information about detected connectivity issues across ASes and locations.', + { + limit: PaginationLimitParam, + offset: PaginationOffsetParam, + dateRange: DateRangeParam.optional(), + dateStart: DateStartParam.optional(), + dateEnd: DateEndParam.optional(), + asn: AsnParam.optional(), + location: LocationParam.optional(), + }, + async ({ limit, offset, dateRange, dateStart, dateEnd, asn, location }) => { + try { + const props = getProps(agent) + const result = await fetchRadarApi(props.accessToken, '/annotations/outages', { + limit, + offset, + dateRange, + dateStart, + dateEnd, + asn, + location, + }) + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ result }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text' as const, + text: `Error getting outages: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + // ============================================================ + // Certificate Transparency Authorities & Logs Tools + // ============================================================ + + agent.server.tool( + 'list_ct_authorities', + 'List Certificate Authorities (CAs) tracked in Certificate Transparency logs.', + { + limit: PaginationLimitParam, + offset: PaginationOffsetParam, + }, + async ({ limit, offset }) => { + try { + const props = getProps(agent) + const result = await fetchRadarApi(props.accessToken, '/ct/authorities', { + limit, + offset, + }) + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ result }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text' as const, + text: `Error listing CT authorities: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + agent.server.tool( + 'get_ct_authority_details', + 'Get details for a specific Certificate Authority by its slug.', + { + caSlug: z.string().describe('The Certificate Authority slug identifier.'), + }, + async ({ caSlug }) => { + try { + const props = getProps(agent) + const result = await fetchRadarApi(props.accessToken, `/ct/authorities/${caSlug}`) + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ result }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text' as const, + text: `Error getting CT authority details: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + agent.server.tool( + 'list_ct_logs', + 'List Certificate Transparency logs.', + { + limit: PaginationLimitParam, + offset: PaginationOffsetParam, + }, + async ({ limit, offset }) => { + try { + const props = getProps(agent) + const result = await fetchRadarApi(props.accessToken, '/ct/logs', { + limit, + offset, + }) + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ result }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text' as const, + text: `Error listing CT logs: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + agent.server.tool( + 'get_ct_log_details', + 'Get details for a specific Certificate Transparency log by its slug.', + { + logSlug: z.string().describe('The Certificate Transparency log slug identifier.'), + }, + async ({ logSlug }) => { + try { + const props = getProps(agent) + const result = await fetchRadarApi(props.accessToken, `/ct/logs/${logSlug}`) + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ result }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text' as const, + text: `Error getting CT log details: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + // ============================================================ + // BGP Additional Tools + // ============================================================ + + agent.server.tool( + 'get_bgp_timeseries', + 'Retrieve BGP updates time series data. Shows BGP announcement and withdrawal patterns over time.', + { + dateRange: DateRangeArrayParam.optional(), + dateStart: DateStartArrayParam.optional(), + dateEnd: DateEndArrayParam.optional(), + asn: AsnArrayParam, + prefix: BgpPrefixArrayParam, + updateType: BgpUpdateTypeParam, + }, + async ({ dateRange, dateStart, dateEnd, asn, prefix, updateType }) => { + try { + const props = getProps(agent) + const result = await fetchRadarApi(props.accessToken, '/bgp/timeseries', { + dateRange, + dateStart, + dateEnd, + asn, + prefix, + updateType, + }) + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ result }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text' as const, + text: `Error getting BGP timeseries: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + agent.server.tool( + 'get_bgp_top_ases', + 'Get top Autonomous Systems by BGP update count.', + { + limit: PaginationLimitParam, + dateRange: DateRangeArrayParam.optional(), + dateStart: DateStartArrayParam.optional(), + dateEnd: DateEndArrayParam.optional(), + asn: AsnArrayParam, + prefix: BgpPrefixArrayParam, + updateType: BgpUpdateTypeParam, + }, + async ({ limit, dateRange, dateStart, dateEnd, asn, prefix, updateType }) => { + try { + const props = getProps(agent) + const result = await fetchRadarApi(props.accessToken, '/bgp/top/ases', { + limit, + dateRange, + dateStart, + dateEnd, + asn, + prefix, + updateType, + }) + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ result }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text' as const, + text: `Error getting BGP top ASes: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + agent.server.tool( + 'get_bgp_top_prefixes', + 'Get top IP prefixes by BGP update count.', + { + limit: PaginationLimitParam, + dateRange: DateRangeArrayParam.optional(), + dateStart: DateStartArrayParam.optional(), + dateEnd: DateEndArrayParam.optional(), + asn: AsnArrayParam, + updateType: BgpUpdateTypeParam, + }, + async ({ limit, dateRange, dateStart, dateEnd, asn, updateType }) => { + try { + const props = getProps(agent) + const result = await fetchRadarApi(props.accessToken, '/bgp/top/prefixes', { + limit, + dateRange, + dateStart, + dateEnd, + asn, + updateType, + }) + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ result }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text' as const, + text: `Error getting BGP top prefixes: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + agent.server.tool( + 'get_bgp_moas', + 'Get Multi-Origin AS (MOAS) prefixes. MOAS occurs when a prefix is announced by multiple ASes, which can indicate hijacking or legitimate anycast.', + { + origin: BgpOriginParam, + prefix: BgpPrefixParam, + invalidOnly: BgpInvalidOnlyParam, + }, + async ({ origin, prefix, invalidOnly }) => { + try { + const props = getProps(agent) + const result = await fetchRadarApi(props.accessToken, '/bgp/routes/moas', { + origin, + prefix, + invalid_only: invalidOnly, + }) + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ result }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text' as const, + text: `Error getting BGP MOAS: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + agent.server.tool( + 'get_bgp_pfx2as', + 'Get prefix-to-ASN mapping. Useful for looking up which AS announces a given IP prefix.', + { + prefix: BgpPrefixParam, + origin: BgpOriginParam, + rpkiStatus: BgpRpkiStatusParam, + longestPrefixMatch: BgpLongestPrefixMatchParam, + }, + async ({ prefix, origin, rpkiStatus, longestPrefixMatch }) => { + try { + const props = getProps(agent) + const result = await fetchRadarApi(props.accessToken, '/bgp/routes/pfx2as', { + prefix, + origin, + rpkiStatus, + longestPrefixMatch, + }) + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ result }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text' as const, + text: `Error getting BGP pfx2as: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + // ============================================================ + // AS Sets and Relationships Tools + // ============================================================ + + agent.server.tool( + 'get_as_set', + 'Get IRR AS-SETs that an Autonomous System is a member of. AS-SETs are used in routing policies.', + { + asn: AsnParam, + }, + async ({ asn }) => { + try { + const props = getProps(agent) + const result = await fetchRadarApi(props.accessToken, `/entities/asns/${asn}/as_set`) + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ result }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text' as const, + text: `Error getting AS set: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + agent.server.tool( + 'get_as_relationships', + 'Get AS-level relationships for an Autonomous System. Shows peer, upstream, and downstream relationships with other ASes.', + { + asn: AsnParam, + asn2: z + .number() + .int() + .positive() + .optional() + .describe('Optional second ASN to check specific relationship.'), + }, + async ({ asn, asn2 }) => { + try { + const props = getProps(agent) + const result = await fetchRadarApi(props.accessToken, `/entities/asns/${asn}/rel`, { + asn2, + }) + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ result }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text' as const, + text: `Error getting AS relationships: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) } diff --git a/apps/radar/src/tools/url-scanner.tools.ts b/apps/radar/src/tools/url-scanner.tools.ts index 1aa58196..46c71323 100644 --- a/apps/radar/src/tools/url-scanner.tools.ts +++ b/apps/radar/src/tools/url-scanner.tools.ts @@ -1,82 +1,259 @@ -import { getCloudflareClient } from '@repo/mcp-common/src/cloudflare-api' +import { z } from 'zod' + import { getProps } from '@repo/mcp-common/src/get-props' -import { pollUntilReady } from '@repo/mcp-common/src/poll' -import { CreateScanResult, UrlParam } from '../types/url-scanner' +import { + CreateScanResult, + ScanIdParam, + ScanVisibilityParam, + ScreenshotResolutionParam, + SearchQueryParam, + SearchSizeParam, + UrlParam, +} from '../types/url-scanner' import type { RadarMCP } from '../radar.app' -const MAX_WAIT_SECONDS = 30 -const INTERVAL_SECONDS = 2 +const URLSCANNER_API_BASE = 'https://api.cloudflare.com/client/v4/accounts' + +type ToolResponse = { + content: Array<{ type: 'text'; text: string }> +} + +/** + * Helper to get account ID or return error response + */ +async function getAccountIdOrError( + agent: RadarMCP +): Promise<{ accountId: string } | { error: ToolResponse }> { + const accountId = await agent.getActiveAccountId() + if (!accountId) { + return { + error: { + content: [ + { + type: 'text' as const, + text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', + }, + ], + }, + } + } + return { accountId } +} export function registerUrlScannerTools(agent: RadarMCP) { + // Search URL scans agent.server.tool( - 'scan_url', - 'Submit a URL to scan', + 'search_url_scans', + "Search URL scans using ElasticSearch-like query syntax. Examples: 'page.domain:example.com', 'verdicts.malicious:true', 'page.asn:AS24940 AND hash:xxx', 'apikey:me AND date:[2025-01 TO 2025-02]'", { - url: UrlParam, + query: SearchQueryParam, + size: SearchSizeParam, }, - async ({ url }) => { - const accountId = await agent.getActiveAccountId() - if (!accountId) { + async ({ query, size }) => { + const result = await getAccountIdOrError(agent) + if ('error' in result) return result.error + + try { + const props = getProps(agent) + const url = new URL(`${URLSCANNER_API_BASE}/${result.accountId}/urlscanner/v2/search`) + if (query) url.searchParams.set('q', query) + if (size) url.searchParams.set('size', String(size)) + + const res = await fetch(url.toString(), { + headers: { Authorization: `Bearer ${props.accessToken}` }, + }) + + if (!res.ok) { + const errorData = await res.json().catch(() => ({})) + throw new Error(`Search failed: ${res.status} ${JSON.stringify(errorData)}`) + } + + const data = await res.json() return { content: [ { - type: 'text', - text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', + type: 'text' as const, + text: JSON.stringify(data), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text' as const, + text: `Error searching scans: ${error instanceof Error ? error.message : String(error)}`, }, ], } } + } + ) + + // Create URL scan + agent.server.tool( + 'create_url_scan', + 'Submit a URL to scan. Returns the scan UUID which can be used to retrieve results.', + { + url: UrlParam, + visibility: ScanVisibilityParam, + screenshotResolution: ScreenshotResolutionParam, + }, + async ({ url, visibility, screenshotResolution }) => { + const result = await getAccountIdOrError(agent) + if ('error' in result) return result.error try { const props = getProps(agent) - const client = getCloudflareClient(props.accessToken) - // Search if there are recent scans for the URL - const scans = await client.urlScanner.scans.list({ - account_id: accountId, - q: `page.url:"${url}"`, - }) + const body: Record = { url } + if (visibility) body.visibility = visibility + if (screenshotResolution) body.screenshotsResolutions = [screenshotResolution] - let scanId = scans.results.length > 0 ? scans.results[0]._id : null + const res = await fetch(`${URLSCANNER_API_BASE}/${result.accountId}/urlscanner/v2/scan`, { + method: 'POST', + headers: { + Authorization: `Bearer ${props.accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }) - if (!scanId) { - // Submit scan - // TODO theres an issue (reported) with this method in the cloudflare TS lib - // const scan = await (client.urlScanner.scans.create({ account_id, url: "https://www.example.com" }, { headers })).withResponse() + if (!res.ok) { + const errorData = await res.json().catch(() => ({})) + throw new Error(`Scan submission failed: ${res.status} ${JSON.stringify(errorData)}`) + } - const res = await fetch( - `https://api.cloudflare.com/client/v4/accounts/${accountId}/urlscanner/v2/scan`, + const scan = CreateScanResult.parse(await res.json()) + return { + content: [ { - method: 'POST', - headers: { - Authorization: `Bearer ${props.accessToken}`, - }, - body: JSON.stringify({ url }), - } - ) - - if (!res.ok) { - throw new Error('Failed to submit scan') + type: 'text' as const, + text: JSON.stringify({ + message: 'Scan submitted successfully', + scanId: scan.uuid, + url, + visibility: visibility || 'Public', + }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text' as const, + text: `Error creating scan: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + // Get URL scan result + agent.server.tool( + 'get_url_scan', + 'Get the results of a URL scan by its UUID. Returns detailed information including verdicts, page info, requests, cookies, and more.', + { + scanId: ScanIdParam, + }, + async ({ scanId }) => { + const result = await getAccountIdOrError(agent) + if ('error' in result) return result.error + + try { + const props = getProps(agent) + + const res = await fetch( + `${URLSCANNER_API_BASE}/${result.accountId}/urlscanner/v2/result/${scanId}`, + { + headers: { Authorization: `Bearer ${props.accessToken}` }, + } + ) + + if (!res.ok) { + if (res.status === 404) { + throw new Error('Scan not found or still in progress') } + const errorData = await res.json().catch(() => ({})) + throw new Error(`Failed to get scan: ${res.status} ${JSON.stringify(errorData)}`) + } - const scan = CreateScanResult.parse(await res.json()) - scanId = scan?.uuid + const data = (await res.json()) as { + verdicts?: unknown + page?: unknown + stats?: unknown + lists?: unknown } + // Return a summary of the most useful fields + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ + verdicts: data.verdicts, + page: data.page, + stats: data.stats, + lists: data.lists, + }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text' as const, + text: `Error getting scan: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + // Get scan screenshot + agent.server.tool( + 'get_url_scan_screenshot', + 'Get the screenshot URL for a completed scan.', + { + scanId: ScanIdParam, + resolution: z + .enum(['desktop', 'mobile', 'tablet']) + .default('desktop') + .optional() + .describe('Screenshot resolution/device type.'), + }, + async ({ scanId, resolution }) => { + const result = await getAccountIdOrError(agent) + if ('error' in result) return result.error - const r = await pollUntilReady({ - taskFn: () => client.urlScanner.scans.get(scanId, { account_id: accountId }), - intervalSeconds: INTERVAL_SECONDS, - maxWaitSeconds: MAX_WAIT_SECONDS, + try { + const props = getProps(agent) + const res = resolution || 'desktop' + + const screenshotUrl = `${URLSCANNER_API_BASE}/${result.accountId}/urlscanner/v2/screenshots/${scanId}.png` + // Verify the screenshot exists + const response = await fetch(screenshotUrl, { + method: 'HEAD', + headers: { Authorization: `Bearer ${props.accessToken}` }, }) + if (!response.ok) { + throw new Error('Screenshot not available. The scan may still be in progress or failed.') + } + return { content: [ { - type: 'text', + type: 'text' as const, text: JSON.stringify({ - result: { verdicts: r.verdicts, stats: r.stats, page: r.page }, // TODO select what is more relevant, or add a param to allow the agent to select a set of metrics + screenshotUrl, + resolution: res, + note: 'Use this URL with Authorization header to download the screenshot', }), }, ], @@ -85,8 +262,59 @@ export function registerUrlScannerTools(agent: RadarMCP) { return { content: [ { - type: 'text', - text: `Error scanning URL: ${error instanceof Error && error.message}`, + type: 'text' as const, + text: `Error getting screenshot: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + } + } + } + ) + + // Get scan HAR + agent.server.tool( + 'get_url_scan_har', + 'Get the HAR (HTTP Archive) data for a completed scan. Contains detailed network request/response information.', + { + scanId: ScanIdParam, + }, + async ({ scanId }) => { + const result = await getAccountIdOrError(agent) + if ('error' in result) return result.error + + try { + const props = getProps(agent) + + const res = await fetch( + `${URLSCANNER_API_BASE}/${result.accountId}/urlscanner/v2/har/${scanId}`, + { + headers: { Authorization: `Bearer ${props.accessToken}` }, + } + ) + + if (!res.ok) { + if (res.status === 404) { + throw new Error('HAR not available. The scan may still be in progress or failed.') + } + const errorData = await res.json().catch(() => ({})) + throw new Error(`Failed to get HAR: ${res.status} ${JSON.stringify(errorData)}`) + } + + const data = await res.json() + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify(data), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text' as const, + text: `Error getting HAR: ${error instanceof Error ? error.message : String(error)}`, }, ], } diff --git a/apps/radar/src/types/radar.ts b/apps/radar/src/types/radar.ts index aefce133..43c8347d 100644 --- a/apps/radar/src/types/radar.ts +++ b/apps/radar/src/types/radar.ts @@ -322,3 +322,519 @@ export const InternetSpeedOrderByParam = z export const InternetQualityMetricParam = z .enum(['BANDWIDTH', 'DNS', 'LATENCY']) .describe('Specifies which metric to return (bandwidth, latency, or DNS response time).') + +// GeoId filter for ADM1 (administrative level 1) filtering +export const GeoIdArrayParam = z + .array(z.string()) + .optional() + .describe( + 'Filters results by Geolocation (ADM1 - administrative level 1, e.g., states/provinces). ' + + 'Provide an array of GeoNames IDs. Prefix with `-` to exclude. ' + + 'Example: ["2267056", "-360689"] includes Lisbon area but excludes another region.' + ) + +// BGP Parameters +export const BgpHijackerAsnParam = z + .number() + .int() + .positive() + .optional() + .describe('Filter by the potential hijacker AS of a BGP hijack event.') + +export const BgpVictimAsnParam = z + .number() + .int() + .positive() + .optional() + .describe('Filter by the potential victim AS of a BGP hijack event.') + +export const BgpInvolvedAsnParam = z + .number() + .positive() + .optional() + .describe('Filter by ASN involved (as hijacker or victim) in a BGP event.') + +export const BgpInvolvedCountryParam = z + .string() + .regex(/^[a-zA-Z]{2}$/) + .optional() + .describe('Filter by country code of the involved AS in a BGP event.') + +export const BgpLeakAsnParam = z + .number() + .positive() + .optional() + .describe('Filter by the leaking AS of a route leak event.') + +export const BgpPrefixParam = z + .string() + .optional() + .describe('Filter by IP prefix (e.g., "1.1.1.0/24").') + +export const BgpMinConfidenceParam = z + .number() + .int() + .min(1) + .max(10) + .optional() + .describe('Filter by minimum confidence score (1-4 low, 5-7 mid, 8+ high).') + +export const BgpMaxConfidenceParam = z + .number() + .int() + .min(1) + .max(10) + .optional() + .describe('Filter by maximum confidence score (1-4 low, 5-7 mid, 8+ high).') + +export const BgpSortByParam = z + .enum(['TIME', 'CONFIDENCE', 'ID']) + .optional() + .describe('Sort results by specified field.') + +export const BgpSortOrderParam = z + .enum(['ASC', 'DESC']) + .optional() + .describe('Sort order (ascending or descending).') + +// Bots Parameters +export const BotsDimensionParam = z + .enum([ + 'timeseries', + 'summary/bot', + 'summary/bot_kind', + 'summary/bot_operator', + 'summary/bot_category', + 'timeseriesGroups/bot', + 'timeseriesGroups/bot_kind', + 'timeseriesGroups/bot_operator', + 'timeseriesGroups/bot_category', + ]) + .describe('Dimension indicating the type and format of bot data to retrieve.') + +export const BotNameParam = z + .array(z.string().max(100)) + .optional() + .describe('Filter results by bot name.') + +export const BotOperatorParam = z + .array(z.string().max(100)) + .optional() + .describe('Filter results by bot operator (e.g., Google, Microsoft, OpenAI).') + +export const BotCategoryParam = z + .array( + z.enum([ + 'SEARCH_ENGINE_CRAWLER', + 'SEARCH_ENGINE_OPTIMIZATION', + 'MONITORING_AND_ANALYTICS', + 'ADVERTISING_AND_MARKETING', + 'SOCIAL_MEDIA_MARKETING', + 'PAGE_PREVIEW', + 'ACADEMIC_RESEARCH', + 'SECURITY', + 'ACCESSIBILITY', + 'WEBHOOKS', + 'FEED_FETCHER', + 'AI_CRAWLER', + 'AGGREGATOR', + 'AI_ASSISTANT', + 'AI_SEARCH', + 'ARCHIVER', + ]) + ) + .optional() + .describe('Filter results by bot category.') + +export const BotKindParam = z + .array(z.enum(['AGENT', 'BOT'])) + .optional() + .describe('Filter results by bot kind (AGENT or BOT).') + +export const BotVerificationStatusParam = z + .array(z.enum(['VERIFIED'])) + .optional() + .describe('Filter results by bot verification status.') + +// Certificate Transparency Parameters +export const CtDimensionParam = z + .enum([ + 'timeseries', + 'summary/ca', + 'summary/caOwner', + 'summary/duration', + 'summary/entryType', + 'summary/expirationStatus', + 'summary/hasIps', + 'summary/hasWildcards', + 'summary/logApi', + 'summary/publicKeyAlgorithm', + 'summary/signatureAlgorithm', + 'summary/validationLevel', + 'timeseriesGroups/ca', + 'timeseriesGroups/caOwner', + 'timeseriesGroups/duration', + 'timeseriesGroups/entryType', + 'timeseriesGroups/expirationStatus', + 'timeseriesGroups/hasIps', + 'timeseriesGroups/hasWildcards', + 'timeseriesGroups/logApi', + 'timeseriesGroups/publicKeyAlgorithm', + 'timeseriesGroups/signatureAlgorithm', + 'timeseriesGroups/validationLevel', + ]) + .describe( + 'Dimension indicating the type and format of Certificate Transparency data to retrieve.' + ) + +export const CtCaParam = z + .array(z.string()) + .optional() + .describe('Filter results by certificate authority.') + +export const CtCaOwnerParam = z + .array(z.string()) + .optional() + .describe('Filter results by certificate authority owner.') + +export const CtDurationParam = z + .array( + z.enum([ + 'LTE_3D', + 'GT_3D_LTE_7D', + 'GT_7D_LTE_10D', + 'GT_10D_LTE_47D', + 'GT_47D_LTE_100D', + 'GT_100D_LTE_200D', + 'GT_200D', + ]) + ) + .optional() + .describe('Filter results by certificate duration.') + +export const CtEntryTypeParam = z + .array(z.enum(['PRECERTIFICATE', 'CERTIFICATE'])) + .optional() + .describe('Filter results by entry type (certificate vs. pre-certificate).') + +export const CtTldParam = z + .array(z.string().min(2).max(63)) + .optional() + .describe('Filter results by top-level domain (e.g., "com", "org").') + +export const CtValidationLevelParam = z + .array(z.enum(['DOMAIN', 'ORGANIZATION', 'EXTENDED'])) + .optional() + .describe('Filter results by validation level (DV, OV, EV).') + +export const CtPublicKeyAlgorithmParam = z + .array(z.enum(['DSA', 'ECDSA', 'RSA'])) + .optional() + .describe('Filter results by public key algorithm.') + +// Netflows Parameters +export const NetflowsDimensionParam = z + .enum(['timeseries', 'summary', 'summary/adm1', 'summary/product', 'top/locations', 'top/ases']) + .describe('Dimension indicating the type and format of NetFlows data to retrieve.') + +export const NetflowsProductParam = z + .array(z.enum(['HTTP', 'ALL'])) + .optional() + .describe('Filter results by network traffic product type.') + +export const NormalizationParam = z + .enum(['RAW_VALUES', 'PERCENTAGE']) + .optional() + .describe('Normalization method applied to results.') + +export const LimitPerGroupParam = z + .number() + .int() + .positive() + .optional() + .describe('Limits the number of items per group. Extra items appear grouped under "other".') + +// Origins/Cloud Observatory Parameters (used by fetch-based tools) +export const OriginSlugParam = z + .enum(['AMAZON', 'GOOGLE', 'MICROSOFT', 'ORACLE']) + .describe( + 'The cloud provider origin to query. Supported values: AMAZON (AWS), GOOGLE (GCP), MICROSOFT (Azure), ORACLE (OCI).' + ) + +export const OriginArrayParam = z + .array(OriginSlugParam) + .min(1) + .describe('Array of cloud provider origins to query. At least one origin must be specified.') + +export const OriginMetricParam = z + .enum([ + 'CONNECTION_FAILURES', + 'REQUESTS', + 'RESPONSE_HEADER_RECEIVE_DURATION', + 'TCP_HANDSHAKE_DURATION', + 'TCP_RTT', + 'TLS_HANDSHAKE_DURATION', + ]) + .describe( + 'The performance metric to retrieve. Only valid when dimension is timeseries or percentile. ' + + 'CONNECTION_FAILURES: Number of failed connections. ' + + 'REQUESTS: Total request count. ' + + 'RESPONSE_HEADER_RECEIVE_DURATION: Time to receive response headers (ms). ' + + 'TCP_HANDSHAKE_DURATION: TCP handshake time (ms). ' + + 'TCP_RTT: TCP round-trip time (ms). ' + + 'TLS_HANDSHAKE_DURATION: TLS handshake time (ms).' + ) + +export const OriginDataDimensionParam = z + .enum([ + 'timeseries', + 'summary/REGION', + 'summary/SUCCESS_RATE', + 'summary/PERCENTILE', + 'timeseriesGroups/REGION', + 'timeseriesGroups/SUCCESS_RATE', + 'timeseriesGroups/PERCENTILE', + ]) + .describe( + 'Dimension indicating the type and format of origins data to retrieve. ' + + 'timeseries: Raw time series data. Requires setting the metric parameter.' + + 'summary/*: Aggregated data grouped by dimension. ' + + 'timeseriesGroups/*: Time series grouped by dimension. ' + + 'REGION: Group by cloud provider region (e.g., us-east-1). ' + + 'SUCCESS_RATE: Group by connection success rate. ' + + 'PERCENTILE: Group by performance percentiles (p50, p90, p99). Requires setting the metric parameter.' + ) + +export const OriginRegionParam = z + .array(z.string().max(100)) + .optional() + .describe( + 'Filters results by cloud provider region. ' + + 'Example regions: us-east-1, eu-west-1, ap-southeast-1.' + ) + +export const OriginNormalizationParam = z + .enum(['PERCENTAGE', 'MIN0_MAX']) + .optional() + .describe('Normalization method for results.') + +// ============================================================ +// Robots.txt Parameters +// ============================================================ + +export const RobotsTxtDimensionParam = z + .enum([ + 'summary/user_agent', + 'timeseries_groups/user_agent', + 'top/domain_categories', + 'top/user_agents/directive', + ]) + .describe('Dimension indicating the type and format of robots.txt data to retrieve.') + +export const RobotsTxtDirectiveParam = z + .enum(['ALLOW', 'DISALLOW']) + .optional() + .describe('Filter by robots.txt directive type (ALLOW or DISALLOW).') + +export const RobotsTxtPatternParam = z + .enum(['FULLY', 'PARTIALLY']) + .optional() + .describe('Filter by pattern matching type (FULLY or PARTIALLY matched).') + +export const RobotsTxtDomainCategoryParam = z + .array(z.string()) + .optional() + .describe('Filter by domain categories.') + +export const RobotsTxtUserAgentCategoryParam = z + .enum(['AI']) + .optional() + .describe('Filter by user agent category (currently only AI is supported).') + +// ============================================================ +// Bots Crawlers Parameters +// ============================================================ + +export const BotsCrawlersDimensionParam = z + .enum(['CLIENT_TYPE', 'USER_AGENT', 'REFERER', 'CRAWL_REFER_RATIO', 'VERTICAL', 'INDUSTRY']) + .describe( + 'Dimension for crawler data. CLIENT_TYPE: crawler type, USER_AGENT: crawler user agent, ' + + 'REFERER: referrer analysis, CRAWL_REFER_RATIO: crawl to referrer ratio, ' + + 'VERTICAL: industry vertical, INDUSTRY: industry classification.' + ) + +export const BotsCrawlersFormatParam = z + .enum(['summary', 'timeseries_groups']) + .describe('Format for crawler data: summary or time series grouped data.') + +export const CrawlerVerticalParam = z + .array(z.string()) + .optional() + .describe('Filter by industry vertical.') + +export const CrawlerIndustryParam = z + .array(z.string()) + .optional() + .describe('Filter by industry classification.') + +export const CrawlerClientTypeParam = z + .array(z.string()) + .optional() + .describe('Filter by client type.') + +// ============================================================ +// Leaked Credential Checks Parameters +// ============================================================ + +export const LeakedCredentialsDimensionParam = z + .enum([ + 'timeseries', + 'summary/compromised', + 'summary/bot_class', + 'timeseries_groups/compromised', + 'timeseries_groups/bot_class', + ]) + .describe('Dimension indicating the type and format of leaked credentials data to retrieve.') + +export const LeakedCredentialsBotClassParam = z + .array(z.string()) + .optional() + .describe('Filter by bot class.') + +export const LeakedCredentialsCompromisedParam = z + .array(z.string()) + .optional() + .describe('Filter by compromised status.') + +// ============================================================ +// AS112 Parameters +// ============================================================ + +export const As112DimensionParam = z + .enum([ + 'timeseries', + 'summary/dnssec', + 'summary/edns', + 'summary/ip_version', + 'summary/protocol', + 'summary/query_type', + 'summary/response_code', + 'timeseries_groups/dnssec', + 'timeseries_groups/edns', + 'timeseries_groups/ip_version', + 'timeseries_groups/protocol', + 'timeseries_groups/query_type', + 'timeseries_groups/response_code', + 'top/locations', + ]) + .describe( + 'Dimension indicating the type and format of AS112 data to retrieve. ' + + 'AS112 is a DNS sink hole for reverse DNS lookups of private IP addresses.' + ) + +export const As112QueryTypeParam = z + .array(z.string()) + .optional() + .describe('Filter by DNS query type.') + +export const As112ProtocolParam = z + .array(z.string()) + .optional() + .describe('Filter by DNS protocol (UDP/TCP).') + +export const As112ResponseCodeParam = z + .array(z.string()) + .optional() + .describe('Filter by DNS response code.') + +// ============================================================ +// TCP Resets/Timeouts Parameters +// ============================================================ + +export const TcpResetsTimeoutsDimensionParam = z + .enum(['summary', 'timeseries_groups']) + .describe('Format for TCP resets/timeouts data: summary or time series grouped data.') + +// ============================================================ +// Annotations/Outages Parameters +// ============================================================ + +export const AnnotationDataSourceParam = z + .enum([ + 'ALL', + 'AI_BOTS', + 'AI_GATEWAY', + 'BGP', + 'BOTS', + 'CONNECTION_ANOMALY', + 'CT', + 'DNS', + 'DNS_MAGNITUDE', + 'DNS_AS112', + 'DOS', + 'EMAIL_ROUTING', + 'EMAIL_SECURITY', + 'FW', + 'FW_PG', + 'HTTP', + 'HTTP_CONTROL', + 'HTTP_CRAWLER_REFERER', + 'HTTP_ORIGINS', + 'IQI', + 'LEAKED_CREDENTIALS', + 'NET', + 'ROBOTS_TXT', + 'SPEED', + 'WORKERS_AI', + ]) + .optional() + .describe('Filter annotations by data source.') + +export const AnnotationEventTypeParam = z + .enum(['EVENT', 'GENERAL', 'OUTAGE', 'PARTIAL_PROJECTION', 'PIPELINE', 'TRAFFIC_ANOMALY']) + .optional() + .describe('Filter annotations by event type.') + +// ============================================================ +// BGP Additional Parameters +// ============================================================ + +export const BgpUpdateTypeParam = z + .array(z.enum(['ANNOUNCEMENT', 'WITHDRAWAL'])) + .optional() + .describe('Filter by BGP update type (ANNOUNCEMENT or WITHDRAWAL).') + +export const BgpPrefixArrayParam = z + .array(z.string()) + .optional() + .describe('Filter by IP prefix(es).') + +export const BgpRpkiStatusParam = z + .enum(['VALID', 'INVALID', 'UNKNOWN']) + .optional() + .describe('Filter by RPKI validation status.') + +export const BgpLongestPrefixMatchParam = z + .boolean() + .optional() + .describe('Whether to use longest prefix match.') + +export const BgpOriginParam = z + .number() + .int() + .positive() + .optional() + .describe('Filter by origin ASN.') + +export const BgpInvalidOnlyParam = z + .boolean() + .optional() + .describe('Only return invalid MOAS prefixes.') + +// ============================================================ +// Geolocation Parameters +// ============================================================ + +export const GeoIdParam = z + .string() + .describe('GeoNames ID for the geolocation (e.g., "2267056" for Lisbon).') diff --git a/apps/radar/src/types/url-scanner.ts b/apps/radar/src/types/url-scanner.ts index 36845101..441e943c 100644 --- a/apps/radar/src/types/url-scanner.ts +++ b/apps/radar/src/types/url-scanner.ts @@ -8,6 +8,38 @@ export const UrlParam = z .url() .describe('A valid URL including protocol (e.g., "https://example.com").') +export const ScanIdParam = z.string().uuid().describe('The UUID of the scan.') + +export const SearchQueryParam = z + .string() + .optional() + .describe( + "ElasticSearch-like query to filter scans. Examples: 'page.domain:example.com', 'verdicts.malicious:true', 'page.asn:AS24940', 'date:[2025-01 TO 2025-02]'" + ) + +export const SearchSizeParam = z + .number() + .int() + .min(1) + .max(100) + .default(10) + .optional() + .describe('Limit the number of results (1-100, default 10).') + +export const ScanVisibilityParam = z + .enum(['Public', 'Unlisted']) + .default('Public') + .optional() + .describe( + 'Scan visibility. Public scans appear in search results. Unlisted scans require the scan ID to access.' + ) + +export const ScreenshotResolutionParam = z + .enum(['desktop', 'mobile', 'tablet']) + .default('desktop') + .optional() + .describe('Screenshot resolution/device type.') + export const CreateScanResult = z .object({ uuid: z.string(),