@jupiterone/jupiterone-mcp
Version:
Model Context Protocol server for JupiterOne account rules and rule details
533 lines • 27 kB
JavaScript
"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