Skip to main content
Glama
radql.ts25.5 kB
import { z } from "zod"; import { RadSecurityClient } from "../client.js"; // ============================================================================ // Schema Definitions // ============================================================================ /** * Schema for listing available data types (discovery) */ export const RadQLListDataTypesSchema = z.object({}).describe( "List all available RadQL data types. Returns data types with descriptions. ALWAYS call this FIRST to discover what data is available." ); /** * Schema for getting metadata about a specific data type (schema discovery) */ export const RadQLGetTypeMetadataSchema = z.object({ data_type: z.string() .describe("The data type to get metadata for (e.g., 'containers', 'kubernetes_resources', 'inbox_items'). Call radql_list_data_types first to see available types.") }).describe( "Get schema/metadata for a specific RadQL data type. Returns available fields, types, and query examples. Call this AFTER radql_list_data_types to understand how to query a data type." ); /** * Schema for listing filter values for a field (enum discovery) */ export const RadQLListFilterValuesSchema = z.object({ data_type: z.string() .describe("The data type (e.g., 'containers', 'kubernetes_resources')"), filter_name: z.string() .describe("The filter field name to get possible values for (e.g., 'namespace', 'cluster_id', 'severity')") }).describe( "List possible values for a filter field. Useful for discovering available namespaces, clusters, severities, etc. Call this when building dynamic filters." ); /** * Main RadQL query schema for executing queries against the Data API */ export const RadQLQuerySchema = z.object({ data_type: z.string() .describe("The data type to query (e.g., 'containers', 'kubernetes_resources', 'inbox_items'). Use radql_list_data_types to discover available types."), operation: z.enum([ "list", "get_by_id", "stats" ]).describe("The operation to perform: 'list' for filtering/searching, 'get_by_id' for single item, 'stats' for aggregations"), filters_query: z.string().optional() .describe("RadQL filter query (e.g., 'severity:High AND type:misconfiguration'). Used for filtering results."), stats_query: z.string().optional() .describe("RadQL analytics query (e.g., 'count() by severity'). Used for aggregations and grouping."), id: z.string().optional() .describe("The ID of a specific item to retrieve (for get_by_id operation)"), limit: z.number().optional().default(20) .describe("Maximum number of results to return"), offset: z.number().optional().default(0) .describe("Pagination offset"), include_relations: z.array(z.string()).optional() .describe("Relations to include (e.g., ['owner'] for containers to include Kubernetes owner resource)") }); /** * Helper schema for building RadQL queries programmatically */ export const RadQLQueryBuilderSchema = z.object({ data_type: z.string() .describe("The data type to build a query for"), conditions: z.array(z.object({ field: z.string(), operator: z.enum([":", "=", "!=", "!:", "<>", ">", ">=", "<", "<=", "contains", "starts_with", "ends_with"]), value: z.union([z.string(), z.number(), z.boolean()]), negate: z.boolean().optional() })).optional() .describe("Filter conditions to combine into a RadQL query"), logic: z.enum(["AND", "OR"]).optional().default("AND") .describe("Logical operator to combine conditions"), aggregation: z.enum(["count", "sum", "avg", "min", "max", "median"]).optional() .describe("Aggregation function to apply"), aggregate_field: z.string().optional() .describe("Field to aggregate (omit for count(*))"), group_by: z.array(z.string()).optional() .describe("Fields to group by"), time_group: z.enum(["second", "minute", "hour", "day", "month", "year"]).optional() .describe("Time-based grouping interval for datetime fields") }); /** * Batch query schema for executing multiple queries in parallel */ export const RadQLBatchQuerySchema = z.object({ queries: z.array(RadQLQuerySchema).max(10) .describe("Array of queries to execute in parallel (max 10)") }); // ============================================================================ // Core Functions // ============================================================================ /** * List all available data types that can be queried */ export async function listDataTypes( client: RadSecurityClient ): Promise<any> { const response = await client.makeRequest( `/accounts/${client.getAccountId()}/data/types` ); return { available_types: Array.isArray(response) ? response : response.available_types || [], hint: "Use radql_get_type_metadata with a specific data_type to see available fields and filtering options" }; } /** * Get metadata for a specific data type including available fields */ export async function getDataTypeMetadata( client: RadSecurityClient, dataType: string ): Promise<any> { const response = await client.makeRequest( `/accounts/${client.getAccountId()}/data/${dataType}/meta` ); return { ...response, radql_examples: generateRadQLExamples(dataType, response.fields || []) }; } /** * List data items with optional filtering */ async function listDataItems( client: RadSecurityClient, dataType: string, params: any ): Promise<any> { const queryParams: Record<string, any> = { limit: params.limit, offset: params.offset }; if (params.filters_query) queryParams.filters_query = params.filters_query; if (params.include_relations && params.include_relations.length > 0) { queryParams.include_relations = params.include_relations.join(","); } const response = await client.makeRequest( `/accounts/${client.getAccountId()}/data/${dataType}`, queryParams ); return { query_explanation: explainRadQLQuery(params.filters_query), total_count: response.total_count || 0, returned_count: response.items?.length || 0, data: response.items || [], fields: response.fields || [], applied_filters: params.filters_query || "none", pagination: { limit: params.limit, offset: params.offset, has_more: response.has_more || false } }; } /** * Get a single data item by ID */ async function getDataItemById( client: RadSecurityClient, dataType: string, id: string ): Promise<any> { const response = await client.makeRequest( `/accounts/${client.getAccountId()}/data/${dataType}/${id}` ); return { data: response.data, fields: response.fields || [], data_type: dataType, id: id }; } /** * Get aggregated statistics for a data type */ async function getDataStats( client: RadSecurityClient, dataType: string, params: any ): Promise<any> { const queryParams: Record<string, any> = { stats_query: params.stats_query }; if (params.filters_query) queryParams.filters_query = params.filters_query; if (params.limit) queryParams.limit = params.limit; if (params.offset) queryParams.offset = params.offset; const response = await client.makeRequest( `/accounts/${client.getAccountId()}/data/stats/${dataType}`, queryParams ); return { query_explanation: explainStatsQuery(params.stats_query), stats_query: params.stats_query, applied_filters: params.filters_query || "none", results: response.items || [], fields: response.fields || [], total_count: response.total_count || 0, returned_count: response.items?.length || 0, has_more: response.has_more || false }; } /** * List available values for a specific filter field */ async function listFilterValues( client: RadSecurityClient, dataType: string, filterName: string ): Promise<any> { const response = await client.makeRequest( `/accounts/${client.getAccountId()}/data/${dataType}/filters/${filterName}/values` ); // Extract values from nested API response structure // API returns: { values: { items: [{data: {value, count}}] } } const items = response.values?.items || []; const values = items.map((item: any) => ({ value: item.data?.value, count: item.data?.count })); return { data_type: dataType, filter_name: filterName, values: values, total_count: response.values?.total_count || 0, has_more: response.values?.has_more || false, hint: `Use these values in your filters_query like: ${filterName}:value` }; } /** * Main function to execute RadQL queries (list, get_by_id, stats) */ export async function executeRadQLQuery( client: RadSecurityClient, args: z.infer<typeof RadQLQuerySchema> ): Promise<any> { const { operation, data_type, ...params } = args; try { switch (operation) { case "list": return await listDataItems(client, data_type, params); case "get_by_id": if (!params.id) { throw new Error("id is required for get_by_id operation"); } return await getDataItemById(client, data_type, params.id); case "stats": if (!params.stats_query) { throw new Error("stats_query is required for stats operation"); } return await getDataStats(client, data_type, params); default: throw new Error(`Unknown operation: ${operation}`); } } catch (error: any) { throw handleRadQLError(error, data_type); } } /** * Wrapper for listing data types (discovery tool) */ export async function executeListDataTypes( client: RadSecurityClient ): Promise<any> { return await listDataTypes(client); } /** * Wrapper for getting type metadata (schema discovery tool) */ export async function executeGetTypeMetadata( client: RadSecurityClient, args: z.infer<typeof RadQLGetTypeMetadataSchema> ): Promise<any> { return await getDataTypeMetadata(client, args.data_type); } /** * Wrapper for listing filter values (enum discovery tool) */ export async function executeListFilterValues( client: RadSecurityClient, args: z.infer<typeof RadQLListFilterValuesSchema> ): Promise<any> { return await listFilterValues(client, args.data_type, args.filter_name); } /** * Build RadQL queries programmatically from structured conditions */ export function buildRadQLQuery( args: z.infer<typeof RadQLQueryBuilderSchema> ): { filters_query?: string; stats_query?: string } { const result: { filters_query?: string; stats_query?: string } = {}; if (args.conditions && args.conditions.length > 0) { const conditions = args.conditions.map(cond => { let query = ""; if (cond.negate) { query += "NOT "; } query += cond.field; // Map operator to RadQL syntax if (cond.operator === "contains") { // Quote wildcard values if they contain special characters const needsQuoting = typeof cond.value === "string" && ( cond.value.includes("-") || cond.value.includes(" ") || cond.value.includes(":") ); const valueStr = needsQuoting ? `"*${cond.value}*"` : `*${cond.value}*`; query += `:${valueStr}`; } else if (cond.operator === "starts_with") { const needsQuoting = typeof cond.value === "string" && ( cond.value.includes("-") || cond.value.includes(" ") || cond.value.includes(":") ); const valueStr = needsQuoting ? `"${cond.value}*"` : `${cond.value}*`; query += `:${valueStr}`; } else if (cond.operator === "ends_with") { const needsQuoting = typeof cond.value === "string" && ( cond.value.includes("-") || cond.value.includes(" ") || cond.value.includes(":") ); const valueStr = needsQuoting ? `"*${cond.value}"` : `*${cond.value}`; query += `:${valueStr}`; } else { // Quote string values to handle dates, hyphens, and special characters // The RadQL parser requires quoting for: // - Dates/timestamps (contain hyphens): "2024-01-01" // - UUIDs (contain hyphens): "550e8400-e29b-41d4-a716-446655440000" // - Strings with special characters: colons, spaces, etc. // - Any value that's not a simple alphanumeric string const value = cond.value; let valueStr: string; if (typeof value === "string") { // Check if the string needs quoting // Quote if it contains: spaces, hyphens, colons, or other special chars // Or if it looks like a date/timestamp const needsQuoting = value.includes(" ") || value.includes("-") || value.includes(":") || value.includes("(") || value.includes(")") || /^\d{4}-\d{2}-\d{2}/.test(value) || /[<>=!]/.test(value); valueStr = needsQuoting ? `"${value}"` : value; } else { valueStr = String(value); } query += `${cond.operator}${valueStr}`; } return query; }); result.filters_query = conditions.join(` ${args.logic} `); } if (args.aggregation) { let statsQuery = ""; if (args.aggregation === "count") { statsQuery = args.aggregate_field ? `count(${args.aggregate_field})` : "count()"; } else { if (!args.aggregate_field) { throw new Error(`${args.aggregation} requires an aggregate_field`); } statsQuery = `${args.aggregation}(${args.aggregate_field})`; } if (args.group_by && args.group_by.length > 0) { const groupByFields = args.group_by.map(field => { if (args.time_group && (field.includes("_at") || field.includes("timestamp") || field.includes("time"))) { return `${args.time_group}(${field})`; } return field; }); statsQuery += ` by ${groupByFields.join(", ")}`; } result.stats_query = statsQuery; } return result; } /** * Execute multiple RadQL queries in parallel */ export async function executeBatchQueries( client: RadSecurityClient, args: z.infer<typeof RadQLBatchQuerySchema> ): Promise<any[]> { const promises = args.queries.map(query => executeRadQLQuery(client, query).catch(err => ({ error: err.message, query: query })) ); return await Promise.all(promises); } // ============================================================================ // Helper Functions // ============================================================================ /** * Generate example RadQL queries for a data type */ function generateRadQLExamples(dataType: string, fields: any[]): any { const examples: any = { filter_examples: [], stats_examples: [] }; // Find fields by type const stringFields = fields.filter((f: any) => f.type === "string" && f.is_filter); const numberFields = fields.filter((f: any) => f.type === "number" && f.is_filter); const booleanFields = fields.filter((f: any) => f.type === "boolean" && f.is_filter); const dateFields = fields.filter((f: any) => f.type === "datetime" && f.is_filter); if (stringFields.length > 0) { examples.filter_examples.push({ description: "Find items with specific text value", query: `${stringFields[0].name}:example_value` }); examples.filter_examples.push({ description: "Find items containing text (wildcard search)", query: `${stringFields[0].name}:*partial*` }); } if (numberFields.length > 0) { examples.filter_examples.push({ description: "Find items with number comparison", query: `${numberFields[0].name}>100` }); } if (booleanFields.length > 0) { examples.filter_examples.push({ description: "Find items by boolean value", query: `${booleanFields[0].name}:true` }); } if (dateFields.length > 0) { examples.filter_examples.push({ description: "Find items after a specific date", query: `${dateFields[0].name}>"2024-01-01"` }); } if (stringFields.length >= 2) { examples.filter_examples.push({ description: "Combine multiple conditions", query: `${stringFields[0].name}:value1 AND ${stringFields[1].name}:value2` }); } examples.stats_examples.push({ description: "Count all items", query: "count()" }); if (stringFields.length > 0) { examples.stats_examples.push({ description: `Count items grouped by ${stringFields[0].name}`, query: `count() by ${stringFields[0].name}` }); } if (numberFields.length > 0) { examples.stats_examples.push({ description: `Average ${numberFields[0].name}`, query: `avg(${numberFields[0].name})` }); if (stringFields.length > 0) { examples.stats_examples.push({ description: `Sum ${numberFields[0].name} grouped by ${stringFields[0].name}`, query: `sum(${numberFields[0].name}) by ${stringFields[0].name}` }); } } if (dateFields.length > 0) { examples.stats_examples.push({ description: "Count items by day", query: `count() by day(${dateFields[0].name})` }); examples.stats_examples.push({ description: "Count items by month", query: `count() by month(${dateFields[0].name})` }); } // Data type specific examples if (dataType === "containers") { examples.filter_examples.push({ description: "Find nginx containers in production", query: 'image_name:*nginx* AND cluster_id:prod*' }); examples.stats_examples.push({ description: "Count containers by image", query: "count() by image_name" }); } if (dataType === "inbox_items") { examples.filter_examples.push({ description: "Find high-severity unresolved items", query: 'severity:High AND NOT archived:true' }); examples.stats_examples.push({ description: "Count items by severity and type", query: "count() by severity, type" }); } if (dataType === "kubernetes_resources") { examples.filter_examples.push({ description: "Find Pods or Services", query: 'kind:Pod OR kind:Service' }); examples.stats_examples.push({ description: "Count resources by kind", query: "count() by kind" }); } return examples; } /** * Explain a RadQL filter query in natural language */ function explainRadQLQuery(query: string | undefined): string { if (!query) return "No filters applied - returning all items"; const explanations: string[] = []; // Parse common patterns if (query.includes(" AND ")) { explanations.push("All conditions must be true"); } if (query.includes(" OR ")) { explanations.push("Any condition can be true"); } if (query.includes("NOT ")) { explanations.push("Excluding items matching certain conditions"); } if (query.includes(":")) { explanations.push("Filtering by exact field values"); } if (query.includes("*")) { explanations.push("Using wildcard pattern matching"); } if (query.includes(">") || query.includes("<")) { explanations.push("Comparing numeric or date values"); } if (query.includes("(") && query.includes(")")) { explanations.push("Using grouped conditions"); } return explanations.length > 0 ? explanations.join("; ") : "Applying custom filter conditions"; } /** * Explain a RadQL stats query in natural language */ function explainStatsQuery(query: string | undefined): string { if (!query) return "No aggregation specified"; const explanations: string[] = []; // Parse aggregation functions if (query.includes("count(")) { explanations.push("Counting items"); } if (query.includes("sum(")) { explanations.push("Summing numeric values"); } if (query.includes("avg(")) { explanations.push("Calculating average values"); } if (query.includes("min(")) { explanations.push("Finding minimum values"); } if (query.includes("max(")) { explanations.push("Finding maximum values"); } if (query.includes("median(")) { explanations.push("Calculating median values"); } // Parse grouping if (query.includes(" by ")) { explanations.push("Grouping results by specified fields"); } // Parse time grouping const timeGroups = ["second(", "minute(", "hour(", "day(", "month(", "year("]; for (const timeGroup of timeGroups) { if (query.includes(timeGroup)) { explanations.push(`Grouping by ${timeGroup.replace("(", "")} intervals`); break; } } return explanations.length > 0 ? explanations.join("; ") : "Applying aggregation functions"; } /** * Enhanced error handling for RadQL queries */ function handleRadQLError(error: any, dataType?: string): Error { const originalMsg = error.message || error.response?.data?.message || String(error); // Parse API error responses if (error.response?.status === 400) { // Detect parser errors (e.g., "unexpected token") if (originalMsg.includes("unexpected token") || originalMsg.includes("invalid query")) { let helpfulMsg = "RadQL Parser Error\n\n"; // Specific error patterns with solutions if (originalMsg.match(/unexpected token.*"-\d+"/)) { helpfulMsg += "ISSUE: Unquoted date or hyphenated value detected\n"; helpfulMsg += "SOLUTION: Dates and values with hyphens MUST be quoted\n\n"; helpfulMsg += "Examples:\n"; helpfulMsg += ' INCORRECT: created_at>2024-01-01\n'; helpfulMsg += ' CORRECT: created_at>"2024-01-01"\n\n'; helpfulMsg += ' INCORRECT: id:abc-123-def\n'; helpfulMsg += ' CORRECT: id:"abc-123-def"\n\n'; } else if (originalMsg.includes("unknown field") || originalMsg.includes("not found")) { helpfulMsg += "ISSUE: Unknown or misspelled field name\n"; helpfulMsg += `SOLUTION: Call radql_get_type_metadata with data_type="${dataType || "your_type"}" to see all available fields\n\n`; // Add common fields based on data type if (dataType === "containers") { helpfulMsg += "Common container fields:\n"; helpfulMsg += " Filterable: name, image_name, image_repo, owner_kind\n"; helpfulMsg += " All: id, cluster_id, image_tag, created_at\n\n"; } else if (dataType === "finding_groups") { helpfulMsg += "Common finding_groups fields:\n"; helpfulMsg += " Filterable: type, severity, source_kind, source_name, rule_title\n"; helpfulMsg += " All: group_id, severity, rule_id, event_timestamp\n\n"; } else if (dataType === "inbox_items") { helpfulMsg += "Common inbox_items fields:\n"; helpfulMsg += " severity (High|Medium|Low), type, title, archived, false_positive, created_at\n\n"; } } else { helpfulMsg += "ISSUE: Invalid query syntax\n"; helpfulMsg += "COMMON SOLUTIONS:\n"; helpfulMsg += ' 1. Quote dates: created_at>"2024-01-01"\n'; helpfulMsg += ' 2. Quote UUIDs: id:"abc-123-def"\n'; helpfulMsg += ' 3. Quote special chars: title:"my value"\n'; helpfulMsg += ` 4. Verify fields: radql_get_type_metadata (data_type="${dataType}")\n\n`; } helpfulMsg += `Original error: ${originalMsg}`; return new Error(helpfulMsg); } // Unknown field error if (originalMsg.includes("unknown field") || originalMsg.includes("field") && originalMsg.includes("not found")) { let helpfulMsg = "Unknown Field Error\n\n"; helpfulMsg += `NEXT STEP: Call radql_get_type_metadata with data_type="${dataType}" for complete field list\n\n`; // Add field suggestions based on data type if (dataType === "containers") { helpfulMsg += "Common container fields (quick reference):\n"; helpfulMsg += " Filterable: name, image_name, image_repo, owner_kind\n"; helpfulMsg += " All: id, cluster_id, image_tag, created_at\n\n"; } else if (dataType === "finding_groups") { helpfulMsg += "Common finding_groups fields (quick reference):\n"; helpfulMsg += " Filterable: type, source_kind, source_name, source_namespace, rule_title\n"; helpfulMsg += " All: group_id, severity, rule_id, event_timestamp\n\n"; } else if (dataType === "inbox_items") { helpfulMsg += "Common inbox_items fields (quick reference):\n"; helpfulMsg += " severity (High|Medium|Low), type, title, archived, false_positive, created_at\n\n"; } helpfulMsg += `Original error: ${originalMsg}`; return new Error(helpfulMsg); } const message = "Invalid RadQL query syntax"; const details = { original_error: originalMsg, next_step: `Call radql_get_type_metadata with data_type="${dataType}" for field list and examples`, data_type: dataType }; return new Error(`${message}\n\n${JSON.stringify(details, null, 2)}`); } if (error.response?.status === 404) { return new Error( `Data type '${dataType}' not found\n\nNEXT STEP: Call radql_list_data_types to see all available types\n\nCommon types: containers, finding_groups, inbox_items, kubernetes_resources` ); } if (error.response?.status === 401) { return new Error( "Authentication failed\n\nVERIFY: RAD Security API credentials are set (BASE_URL and BEARER_TOKEN)" ); } if (error.response?.status === 403) { return new Error( `Access denied to data type '${dataType}'\n\nVERIFY: Account permissions or call radql_list_data_types to confirm type exists` ); } return error; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/rad-security/mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server