UNPKG

@jupiterone/jupiterone-mcp

Version:

Model Context Protocol server for JupiterOne account rules and rule details

533 lines 27 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ruleTools = void 0; const zod_1 = require("zod"); const load_description_js_1 = require("../../utils/load-description.js"); const errors_js_1 = require("./errors.js"); const result_js_1 = require("./result.js"); const types_js_1 = require("./types.js"); const pollingIntervalSchema = zod_1.z.enum([ 'DISABLED', 'THIRTY_MINUTES', 'ONE_HOUR', 'FOUR_HOURS', 'EIGHT_HOURS', 'TWELVE_HOURS', 'ONE_DAY', 'ONE_WEEK', ]); const questionSchema = zod_1.z .object({ queries: zod_1.z.array(zod_1.z.object({ query: zod_1.z.string().describe('J1QL query string'), name: zod_1.z.string().describe('Name identifier for the query'), version: zod_1.z.string().optional().describe('Version of the query'), includeDeleted: zod_1.z.boolean().describe('Whether to include deleted entities'), })), }) .describe('Question configuration'); // Recursive action schema (z.lazy supports nested FOR_EACH_ITEM actions). const actionSchema = zod_1.z.lazy(() => zod_1.z.object({ id: zod_1.z.string().optional(), type: zod_1.z .string() .describe('Action type (e.g., SET_PROPERTY, CREATE_ALERT, SEND_EMAIL, WEBHOOK, FOR_EACH_ITEM)'), targetProperty: zod_1.z.string().optional().describe('Property to set (for SET_PROPERTY actions)'), targetValue: zod_1.z.any().optional().describe('Value to set (for SET_PROPERTY actions)'), integrationInstanceId: zod_1.z .string() .optional() .describe('ID of the integration instance for integration actions'), recipients: zod_1.z.array(zod_1.z.string()).optional().describe('Email recipients for SEND_EMAIL action'), body: zod_1.z .union([zod_1.z.string(), zod_1.z.record(zod_1.z.any())]) .optional() .describe('Message body for email/slack/webhook actions'), channels: zod_1.z.array(zod_1.z.string()).optional().describe('Slack channels for SEND_SLACK_MESSAGE action'), bucket: zod_1.z.string().optional().describe('S3 bucket name for SEND_TO_S3 action'), region: zod_1.z.string().optional().describe('AWS region for SEND_TO_S3 action'), data: zod_1.z.any().optional().describe('Additional data for actions'), entityClass: zod_1.z.string().optional().describe('Entity class for CREATE_JIRA_TICKET action'), summary: zod_1.z.string().optional().describe('Summary for CREATE_JIRA_TICKET action'), issueType: zod_1.z.string().optional().describe('Issue type for CREATE_JIRA_TICKET action'), project: zod_1.z.string().optional().describe('Project key for CREATE_JIRA_TICKET action'), updateContentOnChanges: zod_1.z .boolean() .optional() .describe('Whether to update content on changes for CREATE_JIRA_TICKET action'), additionalFields: zod_1.z.any().optional().describe('Additional fields for CREATE_JIRA_TICKET action'), autoResolve: zod_1.z .boolean() .optional() .describe('Auto-resolve Jira ticket when entity no longer matches (CREATE_JIRA_TICKET)'), resolvedStatus: zod_1.z .string() .optional() .describe('Status to transition to on auto-resolve, e.g. "Closed" (CREATE_JIRA_TICKET)'), entities: zod_1.z.string().optional().describe('Entities for TAG_ENTITIES action'), tags: zod_1.z .array(zod_1.z.object({ name: zod_1.z.string(), value: zod_1.z.string().nullable() })) .optional() .describe('Tags for TAG_ENTITIES action'), method: zod_1.z.string().optional().describe('HTTP method for WEBHOOK action (e.g., POST, GET)'), endpoint: zod_1.z.string().optional().describe('Webhook URL endpoint for WEBHOOK action'), headers: zod_1.z.record(zod_1.z.string()).optional().describe('HTTP headers for WEBHOOK action'), items: zod_1.z .string() .optional() .describe('Template reference to iterate over for FOR_EACH_ITEM (e.g., "{{queries.query0.data}}")'), itemRef: zod_1.z .string() .optional() .describe('Variable name for current item in FOR_EACH_ITEM loop (e.g., "result")'), actions: zod_1.z.array(actionSchema).optional().describe('Nested actions to execute for each item in FOR_EACH_ITEM'), })); const operationsSchema = zod_1.z .array(zod_1.z.object({ when: zod_1.z .object({ type: zod_1.z.literal('FILTER'), condition: zod_1.z .array(zod_1.z.union([zod_1.z.string(), zod_1.z.number(), zod_1.z.array(zod_1.z.union([zod_1.z.string(), zod_1.z.number()]))])) .describe('Filter condition array (e.g., ["AND", ["queries.query0.total", ">", 0]])'), }) .describe('Condition that triggers the actions'), actions: zod_1.z.array(actionSchema).describe('Actions to take when condition is met'), })) .describe('Operations to perform when conditions are met'); const labelsSchema = zod_1.z.array(zod_1.z.object({ labelName: zod_1.z.string(), labelValue: zod_1.z.string().nullable() })); // Output schemas: key fields documented; nested/variant shapes stay open (z.any()) so real data // never false-rejects. The SDK validates structuredContent without stripping the sent payload. const listRulesOutput = { returned: zod_1.z.number(), rules: zod_1.z.array(zod_1.z.record(zod_1.z.any())), pageInfo: zod_1.z.any().optional(), }; const ruleDetailsOutput = { id: zod_1.z.string().optional(), name: zod_1.z.string().optional(), description: zod_1.z.any().optional(), version: zod_1.z.number().nullish(), question: zod_1.z.any().optional(), operations: zod_1.z.array(zod_1.z.any()).optional(), labels: zod_1.z.array(zod_1.z.any()).optional(), tags: zod_1.z.any().optional(), }; const evaluateRuleOutput = { ruleId: zod_1.z.string().optional(), id: zod_1.z.string().optional(), __typename: zod_1.z.string().optional(), }; const ruleMutationOutput = { success: zod_1.z.boolean(), rule: zod_1.z.record(zod_1.z.any()) }; const listRuleEvaluationsOutput = { total: zod_1.z.number(), evaluations: zod_1.z.array(zod_1.z.record(zod_1.z.any())) }; const ruleEvaluationDetailsOutput = { accountRuleId: zod_1.z.any().optional(), startedOn: zod_1.z.any().optional(), question: zod_1.z.any().optional(), conditions: zod_1.z.array(zod_1.z.any()).optional(), actions: zod_1.z.array(zod_1.z.any()).optional(), ruleEvaluationOrigin: zod_1.z.any().optional(), }; const rawDataDownloadUrlOutput = { rawDataKey: zod_1.z.string(), downloadUrl: zod_1.z.string().nullish() }; exports.ruleTools = [ (0, types_js_1.defineTool)({ name: 'list-rules', title: 'List Rules', description: (0, load_description_js_1.loadDescription)('list-rules.md'), inputSchema: { limit: zod_1.z.number().min(1).max(1000).optional(), cursor: zod_1.z.string().optional(), }, outputSchema: listRulesOutput, annotations: types_js_1.READ_ONLY, errorContext: 'Error listing rules', handler: async ({ limit, cursor }, client) => { const response = await client.listRuleInstances(limit, cursor); const instances = response.questionInstances || []; return (0, result_js_1.structuredResult)({ returned: instances.length, rules: instances.map((instance) => ({ id: instance.id, name: instance.name, description: instance.description, version: instance.version, pollingInterval: instance.pollingInterval, lastEvaluationStartOn: instance.lastEvaluationStartOn, lastEvaluationEndOn: instance.lastEvaluationEndOn, latestAlertId: instance.latestAlertId, latestAlertIsActive: instance.latestAlertIsActive, type: instance.type, tags: instance.tags, outputs: instance.outputs, })), pageInfo: response.pageInfo, }); }, }), (0, types_js_1.defineTool)({ name: 'get-rule-details', title: 'Get Rule Details', description: (0, load_description_js_1.loadDescription)('get-rule-details.md'), inputSchema: { ruleId: zod_1.z.string(), }, outputSchema: ruleDetailsOutput, annotations: types_js_1.READ_ONLY, errorContext: 'Error getting rule details', handler: async ({ ruleId }, client) => { const instances = await client.getAllRuleInstances(); const rule = instances.find((instance) => instance.id === ruleId); if (!rule) { return { content: [{ type: 'text', text: `Rule with ID ${ruleId} not found` }], isError: true, }; } return (0, result_js_1.structuredResult)({ id: rule.id, resourceGroupId: rule.resourceGroupId, accountId: rule.accountId, name: rule.name, description: rule.description, version: rule.version, lastEvaluationStartOn: rule.lastEvaluationStartOn, lastEvaluationEndOn: rule.lastEvaluationEndOn, evaluationStep: rule.evaluationStep, specVersion: rule.specVersion, notifyOnFailure: rule.notifyOnFailure, triggerActionsOnNewEntitiesOnly: rule.triggerActionsOnNewEntitiesOnly, ignorePreviousResults: rule.ignorePreviousResults, pollingInterval: rule.pollingInterval, templates: rule.templates, outputs: rule.outputs, labels: rule.labels?.map((label) => ({ labelName: label.labelName, labelValue: label.labelValue, })) || [], question: rule.question ? { queries: rule.question.queries?.map((query) => ({ query: query.query, name: query.name, includeDeleted: query.includeDeleted, })) || [], } : null, questionId: rule.questionId, latest: rule.latest, deleted: rule.deleted, type: rule.type, operations: rule.operations?.map((op) => ({ when: op.when, actions: op.actions })) || [], latestAlertId: rule.latestAlertId, latestAlertIsActive: rule.latestAlertIsActive, state: rule.state ? { actions: rule.state.actions } : null, tags: rule.tags, remediationSteps: rule.remediationSteps, }); }, }), (0, types_js_1.defineTool)({ name: 'evaluate-rule', title: 'Evaluate Rule', description: (0, load_description_js_1.loadDescription)('evaluate-rule.md'), inputSchema: { ruleId: zod_1.z.string(), }, outputSchema: evaluateRuleOutput, annotations: types_js_1.MUTATE, errorContext: 'Error evaluating rule', handler: async ({ ruleId }, client) => { const result = await client.evaluateRuleInstance(ruleId); return (0, result_js_1.structuredResult)({ ruleId, id: result.id, __typename: result.__typename }); }, }), (0, types_js_1.defineTool)({ name: 'create-inline-question-rule', title: 'Create Inline Question Rule', description: (0, load_description_js_1.loadDescription)('create-inline-question-rule.md'), inputSchema: { name: zod_1.z.string().describe('Name of the rule'), description: zod_1.z.string().describe('Description of the rule'), notifyOnFailure: zod_1.z.boolean().optional().describe('Whether to notify on failure'), triggerActionsOnNewEntitiesOnly: zod_1.z .boolean() .optional() .describe('Whether to trigger actions only on new entities'), ignorePreviousResults: zod_1.z.boolean().optional().describe('Whether to ignore previous results'), pollingInterval: pollingIntervalSchema.describe('How frequently to evaluate the rule'), outputs: zod_1.z.array(zod_1.z.string()).describe('Output fields from the rule evaluation'), specVersion: zod_1.z.number().describe('Specification version (required by the API; use 1)'), tags: zod_1.z.array(zod_1.z.string()).optional().describe('Tags for categorizing the rule'), templates: zod_1.z.record(zod_1.z.any()).optional().describe('Template variables'), question: questionSchema, labels: labelsSchema.describe('Labels for the rule'), operations: operationsSchema, }, outputSchema: ruleMutationOutput, annotations: types_js_1.CREATE, errorContext: 'Error creating inline question rule', handler: async (params, client, validator) => { const validationResults = await (0, errors_js_1.validateQueries)(params.question.queries, validator); if (validationResults.length > 0) { return (0, errors_js_1.createValidationErrorResponse)(validationResults); } // Built as an intermediate value so the (deliberate) `labels` field, absent from the input // type, is forwarded to the API as the original implementation did. Typing the rule shapes // is C3 (Phase 3). const instance = { name: params.name, description: params.description, notifyOnFailure: params.notifyOnFailure, triggerActionsOnNewEntitiesOnly: params.triggerActionsOnNewEntitiesOnly, ignorePreviousResults: params.ignorePreviousResults, pollingInterval: params.pollingInterval, outputs: params.outputs, specVersion: params.specVersion, labels: params.labels, tags: params.tags, templates: params.templates, question: params.question, operations: params.operations, }; const result = await client.createInlineQuestionRuleInstance(instance); const accountInfo = await client.getAccountInfo(); const ruleUrl = client.ruleService.constructRuleUrl(result.id, accountInfo.subdomain); return (0, result_js_1.structuredResult)({ success: true, rule: { id: result.id, name: result.name, description: result.description, version: result.version, pollingInterval: result.pollingInterval, outputs: result.outputs, specVersion: result.specVersion, notifyOnFailure: result.notifyOnFailure, triggerActionsOnNewEntitiesOnly: result.triggerActionsOnNewEntitiesOnly, ignorePreviousResults: result.ignorePreviousResults, tags: result.tags, question: result.question, operations: result.operations, latestAlertId: result.latestAlertId, latestAlertIsActive: result.latestAlertIsActive, url: ruleUrl, }, }); }, }), (0, types_js_1.defineTool)({ name: 'update-inline-question-rule', title: 'Update Inline Question Rule', description: (0, load_description_js_1.loadDescription)('update-inline-question-rule.md'), inputSchema: { id: zod_1.z.string().describe('ID of the rule to update'), name: zod_1.z.string().describe('Name of the rule'), description: zod_1.z.string().describe('Description of the rule'), notifyOnFailure: zod_1.z.boolean().describe('Whether to notify on failure'), triggerActionsOnNewEntitiesOnly: zod_1.z .boolean() .describe('Whether to trigger actions only on new entities'), ignorePreviousResults: zod_1.z.boolean().describe('Whether to ignore previous results'), pollingInterval: pollingIntervalSchema.describe('How frequently to evaluate the rule'), outputs: zod_1.z.array(zod_1.z.string()).describe('Output fields from the rule evaluation'), version: zod_1.z.number().describe('Current version number of the rule (from get-rule-details)'), specVersion: zod_1.z.number().describe('Specification version'), tags: zod_1.z.array(zod_1.z.string()).describe('Tags for categorizing the rule'), templates: zod_1.z.record(zod_1.z.any()).describe('Template variables'), labels: labelsSchema.describe('Labels for the rule'), resourceGroupId: zod_1.z.string().nullable().describe('Resource group ID'), remediationSteps: zod_1.z.string().nullable().describe('Steps to remediate issues found by the rule'), question: questionSchema, operations: operationsSchema.describe('Operations that define when and what actions to take'), }, outputSchema: ruleMutationOutput, annotations: types_js_1.MUTATE, errorContext: 'Error updating inline question rule', handler: async (params, client, validator) => { const validationResults = await (0, errors_js_1.validateQueries)(params.question?.queries, validator); if (validationResults.length > 0) { return (0, errors_js_1.createValidationErrorResponse)(validationResults); } // See create-inline-question-rule: intermediate value preserves the original `labels` forward. const instance = { id: params.id, name: params.name, description: params.description, notifyOnFailure: params.notifyOnFailure, triggerActionsOnNewEntitiesOnly: params.triggerActionsOnNewEntitiesOnly, ignorePreviousResults: params.ignorePreviousResults, pollingInterval: params.pollingInterval, outputs: params.outputs, version: params.version, specVersion: params.specVersion, labels: params.labels, tags: params.tags, templates: params.templates, resourceGroupId: params.resourceGroupId, remediationSteps: params.remediationSteps, question: params.question, operations: params.operations, }; const result = await client.updateInlineQuestionRuleInstance(instance); const accountInfo = await client.getAccountInfo(); const ruleUrl = client.ruleService.constructRuleUrl(result.id, accountInfo.subdomain); return (0, result_js_1.structuredResult)({ success: true, rule: { id: result.id, name: result.name, description: result.description, version: result.version, pollingInterval: result.pollingInterval, outputs: result.outputs, specVersion: result.specVersion, notifyOnFailure: result.notifyOnFailure, triggerActionsOnNewEntitiesOnly: result.triggerActionsOnNewEntitiesOnly, ignorePreviousResults: result.ignorePreviousResults, tags: result.tags, labels: result.labels, question: result.question, operations: result.operations, latestAlertId: result.latestAlertId, latestAlertIsActive: result.latestAlertIsActive, resourceGroupId: result.resourceGroupId, remediationSteps: result.remediationSteps, url: ruleUrl, }, }); }, }), (0, types_js_1.defineTool)({ name: 'list-rule-evaluations', title: 'List Rule Evaluations', description: (0, load_description_js_1.loadDescription)('list-rule-evaluations.md'), inputSchema: { ruleId: zod_1.z.string(), beginTimestamp: zod_1.z.number().optional(), endTimestamp: zod_1.z.number().optional(), limit: zod_1.z.number().min(1).max(1000).optional(), tag: zod_1.z.string().optional(), }, outputSchema: listRuleEvaluationsOutput, annotations: types_js_1.READ_ONLY, errorContext: 'Error listing rule evaluations', handler: async ({ ruleId, beginTimestamp, endTimestamp, limit, tag }, client) => { const evaluations = await client.getAllRuleEvaluations({ collectionType: 'RULE_EVALUATION', collectionOwnerId: ruleId, beginTimestamp: beginTimestamp || 0, endTimestamp: endTimestamp || Date.now(), limit, tag, }); return (0, result_js_1.structuredResult)({ total: evaluations.length, evaluations: evaluations.map((evaluation) => ({ accountId: evaluation.accountId, collectionOwnerId: evaluation.collectionOwnerId, collectionOwnerVersion: evaluation.collectionOwnerVersion, collectionType: evaluation.collectionType, outputs: evaluation.outputs, rawDataDescriptors: evaluation.rawDataDescriptors, tag: evaluation.tag, timestamp: evaluation.timestamp, })), }); }, }), (0, types_js_1.defineTool)({ name: 'get-rule-evaluation-details', title: 'Get Rule Evaluation Details', description: (0, load_description_js_1.loadDescription)('get-rule-evaluation-details.md'), inputSchema: { ruleId: zod_1.z.string(), timestamp: zod_1.z.number(), }, outputSchema: ruleEvaluationDetailsOutput, annotations: types_js_1.READ_ONLY, errorContext: 'Error getting rule evaluation details', handler: async ({ ruleId, timestamp }, client) => { const details = await client.getRuleEvaluationDetails({ ruleInstanceId: ruleId, timestamp }); return (0, result_js_1.structuredResult)({ accountRuleId: details.accountRuleId, startedOn: details.startedOn, question: details.question ? { totalDuration: details.question.totalDuration, queries: Array.isArray(details.question.queries) ? details.question.queries.map((query) => ({ status: query.status, queryEvaluationDetails: Array.isArray(query.queryEvaluationDetails) ? query.queryEvaluationDetails.map((detail) => ({ name: detail.name, duration: detail.duration, status: detail.status, error: detail.error, })) : [], })) : [], } : // Omit rather than null: the output schema declares `question` `.optional()`, which // accepts an absent/undefined field but rejects an explicit null. undefined, conditions: Array.isArray(details.conditions) ? details.conditions.map((condition) => ({ status: condition.status, condition: condition.condition, })) : [], actions: Array.isArray(details.actions) ? details.actions.map((action) => ({ status: action.status, actionEvaluationDetails: Array.isArray(action.actionEvaluationDetails) ? action.actionEvaluationDetails.map((detail) => ({ actionId: detail.actionId, action: detail.action, status: detail.status, duration: detail.duration, finishedOn: detail.finishedOn, logs: detail.logs, })) : [], })) : [], ruleEvaluationOrigin: details.ruleEvaluationOrigin, }); }, }), (0, types_js_1.defineTool)({ name: 'get-raw-data-download-url', title: 'Get Raw Data Download URL', description: (0, load_description_js_1.loadDescription)('get-raw-data-download-url.md'), inputSchema: { rawDataKey: zod_1.z.string(), }, outputSchema: rawDataDownloadUrlOutput, annotations: types_js_1.READ_ONLY, errorContext: 'Error getting raw data download URL', handler: async ({ rawDataKey }, client) => { const downloadUrl = await client.getRawDataDownloadUrl(rawDataKey); return (0, result_js_1.structuredResult)({ rawDataKey, downloadUrl }); }, }), (0, types_js_1.defineTool)({ name: 'get-rule-evaluation-query-results', title: 'Get Rule Evaluation Query Results', description: (0, load_description_js_1.loadDescription)('get-rule-evaluation-query-results.md'), inputSchema: { rawDataKey: zod_1.z.string(), }, // No outputSchema: the downloaded raw results are an opaque, variable payload (often a bare // array), so a fixed object schema would false-reject. Stays content-only (JSON text). annotations: types_js_1.READ_ONLY, errorContext: 'Error getting rule evaluation query results', handler: async ({ rawDataKey }, client) => { const results = await client.getRawDataResults(rawDataKey); return { content: [{ type: 'text', text: JSON.stringify(results) }], }; }, }), ]; //# sourceMappingURL=rules.js.map