diff --git a/components/elastic_security/actions/add-case-comment/add-case-comment.mjs b/components/elastic_security/actions/add-case-comment/add-case-comment.mjs new file mode 100644 index 0000000000000..f12a0618ee334 --- /dev/null +++ b/components/elastic_security/actions/add-case-comment/add-case-comment.mjs @@ -0,0 +1,50 @@ +import elasticSecurity from "../../elastic_security.app.mjs"; +import { + CASE_COMMENT_TYPE_USER, CASE_OWNER, +} from "../../common/constants.mjs"; + +export default { + key: "elastic_security-add-case-comment", + name: "Add Case Comment", + description: "Add a user comment to an Elastic Security case via POST /api/cases/{caseId}/comments." + + " Use this to log investigation notes or updates on a case without changing its status or fields — use **Create or Update Case** for that." + + " Run **Find Cases** first to obtain a valid case ID." + + " Example: calling with `caseId: \"a1c1...\"` and `comment: \"Confirmed unauthorized access via badge logs.\"` returns the updated case object with `totalComment` incremented and the new comment in `comments`." + + " [See the documentation](https://www.elastic.co/docs/api/doc/kibana/operation/operation-addcasecommentdefaultspace)", + version: "0.0.1", + type: "action", + ai: "optimized", + annotations: { + readOnlyHint: false, + destructiveHint: false, + openWorldHint: true, + }, + props: { + elasticSecurity, + caseId: { + propDefinition: [ + elasticSecurity, + "caseId", + ], + description: "The ID of the case to comment on. Run **Find Cases** first to obtain valid case IDs.", + }, + comment: { + type: "string", + label: "Comment", + description: "The text of the user comment to add.", + }, + }, + async run({ $ }) { + const response = await this.elasticSecurity.addCaseComment({ + $, + caseId: this.caseId, + data: { + type: CASE_COMMENT_TYPE_USER, + comment: this.comment, + owner: CASE_OWNER, + }, + }); + $.export("$summary", `Added comment to case ${this.caseId}`); + return response; + }, +}; diff --git a/components/elastic_security/actions/create-or-update-case/create-or-update-case.mjs b/components/elastic_security/actions/create-or-update-case/create-or-update-case.mjs new file mode 100644 index 0000000000000..c8fa11dc15315 --- /dev/null +++ b/components/elastic_security/actions/create-or-update-case/create-or-update-case.mjs @@ -0,0 +1,154 @@ +import { ConfigurationError } from "@pipedream/platform"; +import elasticSecurity from "../../elastic_security.app.mjs"; +import { + CASE_OWNER, DEFAULT_CASE_CONNECTOR, +} from "../../common/constants.mjs"; + +export default { + key: "elastic_security-create-or-update-case", + name: "Create or Update Case", + description: "Create a new Elastic Security case, or update an existing one when `caseId` is provided, via POST /api/cases or PATCH /api/cases." + + " Use this to open a new case, or to edit a case's title, description, severity, tags, category, assignees, or status." + + " When `caseId` is provided, the tool fetches the case's current `version` internally before updating — never guess or supply a version yourself." + + " Run **Find Cases** first to obtain a `caseId` for updates. Use **Add Case Comment** to attach comments instead of this tool." + + " `title` and `description` are required when creating (no `caseId`)." + + " Example: calling with `title: \"Perimeter Breach\"`, `description: \"...\"`, `severity: \"high\"` returns `{ id: \"a1c1...\", title: \"Perimeter Breach\", status: \"open\", version: \"Wzc1LDFd\", ... }`; calling again with that `caseId` and `status: \"closed\"` returns the same case updated." + + " [See the create documentation](https://www.elastic.co/docs/api/doc/kibana/operation/operation-createcasedefaultspace) and the [update documentation](https://www.elastic.co/docs/api/doc/kibana/operation/operation-updatecasedefaultspace)", + version: "0.0.1", + type: "action", + ai: "optimized", + annotations: { + readOnlyHint: false, + destructiveHint: false, + openWorldHint: true, + }, + props: { + elasticSecurity, + caseId: { + propDefinition: [ + elasticSecurity, + "caseId", + ], + description: "The ID of an existing case to update. Omit this to create a new case instead. Run **Find Cases** first to obtain valid case IDs.", + optional: true, + }, + title: { + type: "string", + label: "Title", + description: "Case title (max 160 characters). Required when creating a new case (no `caseId`).", + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "Case description (max 30000 characters). Required when creating a new case (no `caseId`).", + optional: true, + }, + severity: { + propDefinition: [ + elasticSecurity, + "severity", + ], + description: "Case severity. One of: `low`, `medium`, `high`, `critical`.", + optional: true, + }, + status: { + propDefinition: [ + elasticSecurity, + "status", + ], + description: "New case status. Only applies when updating an existing case (`caseId` provided) — the create API has no status field.", + optional: true, + }, + tags: { + propDefinition: [ + elasticSecurity, + "tags", + ], + description: "Tags to apply to the case. Run **List Tags** first to reuse existing tags instead of creating near-duplicates. On update, this replaces the case's existing tag set entirely.", + optional: true, + }, + category: { + type: "string", + label: "Category", + description: "Case category (max 50 characters).", + optional: true, + }, + assignees: { + type: "string[]", + label: "Assignees", + description: "User profile IDs to assign to the case (max 10). Example: `[\"u_abc123\"]`. Run **Find Assignable Users** first to discover valid `profile_uid` values. On update, this replaces the case's existing assignee set entirely.", + optional: true, + }, + syncAlerts: { + type: "boolean", + label: "Sync Alerts", + description: "Whether to sync the status of attached alerts with the case status. Defaults to `true` on create.", + optional: true, + }, + }, + async run({ $ }) { + const assignees = this.assignees + ? this.assignees.map((uid) => ({ + uid, + })) + : undefined; + + if (!this.caseId) { + if (!this.title || !this.description) { + throw new ConfigurationError("`title` and `description` are required when creating a new case (no `caseId` provided)."); + } + const response = await this.elasticSecurity.createCase({ + $, + data: { + title: this.title, + description: this.description, + severity: this.severity, + tags: this.tags ?? [], + category: this.category, + assignees, + settings: { + syncAlerts: this.syncAlerts ?? true, + }, + connector: DEFAULT_CASE_CONNECTOR, + owner: CASE_OWNER, + }, + }); + $.export("$summary", `Created case "${response.title}" (${response.id})`); + return response; + } + + const current = await this.elasticSecurity.getCase({ + $, + caseId: this.caseId, + }); + const response = await this.elasticSecurity.updateCase({ + $, + data: { + cases: [ + { + id: this.caseId, + version: current.version, + title: this.title, + description: this.description, + severity: this.severity, + status: this.status, + tags: this.tags, + category: this.category, + assignees, + settings: this.syncAlerts === undefined + ? undefined + : { + syncAlerts: this.syncAlerts, + }, + }, + ], + }, + }); + const [ + updated, + ] = response; + $.export("$summary", `Updated case "${updated.title}" (${updated.id})`); + return updated; + }, +}; diff --git a/components/elastic_security/actions/create-or-update-detection-rule/create-or-update-detection-rule.mjs b/components/elastic_security/actions/create-or-update-detection-rule/create-or-update-detection-rule.mjs new file mode 100644 index 0000000000000..d1898e5eaadbc --- /dev/null +++ b/components/elastic_security/actions/create-or-update-detection-rule/create-or-update-detection-rule.mjs @@ -0,0 +1,255 @@ +import { ConfigurationError } from "@pipedream/platform"; +import elasticSecurity from "../../elastic_security.app.mjs"; +import { + RULE_TYPES, RULE_READ_ONLY_FIELDS, +} from "../../common/constants.mjs"; + +export default { + key: "elastic_security-create-or-update-detection-rule", + name: "Create or Update Detection Rule", + description: "Create a new Elastic Security detection rule via POST /api/detection_engine/rules, or full-replace update an existing one when `id` is provided, via PUT /api/detection_engine/rules." + + " On update, the tool first fetches the rule's current definition and merges your supplied fields into it, so you only need to pass the fields you want to change — Kibana's underlying PUT still requires the full definition, but this tool handles that for you." + + " Run **Find Detection Rules** first to obtain the `id` for updates (it also accepts `ruleId` if that's all you have)." + + " `name`, `description`, `riskScore`, `severity`, and `type` are required when creating (no `id`); optionally set `ruleId` on create to assign a custom `rule_id` instead of letting Kibana generate one." + + " For `type: threshold` rules, set `threshold`. For `type: threat_match` rules, set `threatIndex` and `threatMapping`. Use `additionalFields` as an escape hatch for any other type-specific fields (e.g. `anomaly_threshold` for `machine_learning` rules)." + + " Example: calling with `name: \"Suspicious PowerShell\"`, `description: \"...\"`, `riskScore: 60`, `severity: \"high\"`, `type: \"query\"`, `query: \"process.name: powershell.exe\"` returns `{ id: \"7ac3...\", rule_id: \"f3bb...\", name: \"Suspicious PowerShell\", enabled: true, ... }`; calling again with that `id` and `riskScore: 80` returns the same rule with only the risk score changed." + + " [See the create documentation](https://www.elastic.co/docs/api/doc/kibana/operation/operation-createrule) and the [update documentation](https://www.elastic.co/docs/api/doc/kibana/operation/operation-updaterule)", + version: "0.0.1", + type: "action", + ai: "optimized", + annotations: { + readOnlyHint: false, + destructiveHint: false, + openWorldHint: true, + }, + props: { + elasticSecurity, + id: { + propDefinition: [ + elasticSecurity, + "id", + ], + description: "The Kibana internal UUID of an existing rule to update (e.g. `7ac3c66d-f0b4-4f7c-a576-7bb91bf4e9ce`). This is the sole trigger for update mode — omit it to create a new rule. Run **Find Detection Rules** first to obtain valid IDs (it accepts either `id` or `ruleId` for lookup).", + optional: true, + }, + ruleId: { + propDefinition: [ + elasticSecurity, + "ruleId", + ], + description: "When creating (no `id`): an optional custom `rule_id` to assign to the new rule, e.g. `my-custom-rule-id` — if omitted, Kibana generates one. Not used to identify a rule for update; use `id` for that (run **Find Detection Rules** with `ruleId` first if that's all you have, to get its `id`).", + optional: true, + }, + name: { + type: "string", + label: "Name", + description: "Human-readable rule name (e.g. `Suspicious PowerShell Execution`). Required when creating.", + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "Description of what the rule detects. Required when creating.", + optional: true, + }, + riskScore: { + type: "integer", + label: "Risk Score", + description: "Risk score from 0 to 100. Required when creating.", + optional: true, + min: 0, + max: 100, + }, + severity: { + propDefinition: [ + elasticSecurity, + "severity", + ], + description: "Rule severity. One of: `low`, `medium`, `high`, `critical`. Required when creating.", + optional: true, + }, + type: { + type: "string", + label: "Type", + description: "Rule type discriminator. One of: `query`, `eql`, `saved_query`, `threshold`, `threat_match`, `machine_learning`, `new_terms`, `esql`. Required when creating. Cannot be changed on update.", + optional: true, + options: RULE_TYPES, + }, + query: { + type: "string", + label: "Query", + description: "Detection query in KQL or Lucene (required for `query`/`saved_query`/`eql` style rules), e.g. `process.name: powershell.exe`.", + optional: true, + }, + language: { + type: "string", + label: "Language", + description: "Query language: `kuery` or `lucene`.", + optional: true, + options: [ + "kuery", + "lucene", + ], + }, + index: { + type: "string[]", + label: "Index Patterns", + description: "Index patterns the rule runs against (e.g. `logs-*`, `winlogbeat-*`).", + optional: true, + }, + enabled: { + type: "boolean", + label: "Enabled", + description: "Whether the rule is enabled. Defaults to `true` on create.", + optional: true, + }, + tags: { + propDefinition: [ + elasticSecurity, + "tags", + ], + description: "Tags to apply to the rule. Run **List Tags** first to reuse existing tags instead of creating near-duplicates. On update, this replaces the rule's existing tag set entirely.", + optional: true, + }, + interval: { + type: "string", + label: "Interval", + description: "How often the rule runs, as date-math (e.g. `5m`). Defaults to `5m` on create.", + optional: true, + }, + from: { + type: "string", + label: "From", + description: "Start of the rule's lookback window as date-math (e.g. `now-6m`). Defaults to `now-6m` on create.", + optional: true, + }, + maxSignals: { + type: "integer", + label: "Max Signals", + description: "Maximum number of alerts the rule can create per run. Minimum 1, maximum 1000. Defaults to 100 on create.", + optional: true, + min: 1, + max: 1000, + }, + threshold: { + type: "object", + label: "Threshold", + description: "Threshold configuration, required for `type: threshold` rules. Example: `{\"field\":[\"host.name\"],\"value\":5}` fires when 5+ matching events share the same `host.name`.", + optional: true, + }, + threatIndex: { + type: "string[]", + label: "Threat Index", + description: "Index patterns containing threat intelligence indicators, required for `type: threat_match` rules. Example: `[\"logs-ti_*\"]`.", + optional: true, + }, + threatMapping: { + type: "object", + label: "Threat Mapping", + description: "A single threat-match group, required for `type: threat_match` rules. Shape: `{\"entries\":[{\"field\":\"source.ip\",\"type\":\"mapping\",\"value\":\"threat.indicator.ip\"}]}`, matching a local event field against a threat indicator field. For multiple match groups, use `additionalFields.threat_mapping` (an array of these objects) instead.", + optional: true, + }, + additionalFields: { + type: "object", + label: "Additional Fields", + description: "Additional rule fields to merge into the request body, for type-specific configuration not covered by other parameters (e.g. `{\"anomaly_threshold\":50,\"machine_learning_job_id\":[\"job-1\"]}` for `machine_learning` rules, or `threat_mapping` as an array for multi-group `threat_match` rules, since `threatMapping` only supports one group). `id`, `rule_id`, and `type` here are always ignored — use the dedicated `type` parameter instead. Read-only fields (`created_at`, `updated_at`, `revision`, etc.) are also always ignored, even though they have no dedicated parameter of their own. Any other key here is ignored if you've also set its dedicated parameter (that value wins); otherwise it's used as given.", + optional: true, + }, + }, + async run({ $ }) { + const namedFields = { + name: this.name, + description: this.description, + risk_score: this.riskScore, + severity: this.severity, + type: this.type, + query: this.query, + language: this.language, + index: this.index, + enabled: this.enabled, + tags: this.tags, + interval: this.interval, + from: this.from, + max_signals: this.maxSignals, + threshold: this.threshold, + threat_index: this.threatIndex, + threat_mapping: this.threatMapping && [ + this.threatMapping, + ], + }; + const suppliedNamedFields = Object.fromEntries( + Object.entries(namedFields).filter(([ + , value, + ]) => value !== undefined), + ); + // additionalFields is a free-form escape hatch. A field the caller actually supplied via a + // dedicated prop always wins over it (e.g. threshold). `id`/`rule_id`/`type` and read-only + // fields are protected even when unset — silently accepting them from additionalFields would + // let a caller change a rule's identity or type without going through the validated props. + // Everything else (e.g. `threat_mapping` for multi-group threat_match rules) can flow through + // additionalFields when its dedicated prop isn't used, per this tool's documented escape hatch. + const alwaysProtectedKeys = new Set([ + "id", + "rule_id", + "type", + ...RULE_READ_ONLY_FIELDS, + ]); + const protectedKeys = new Set([ + ...alwaysProtectedKeys, + ...Object.keys(suppliedNamedFields), + ]); + const safeAdditionalFields = Object.fromEntries( + Object.entries(this.additionalFields ?? {}).filter(([ + key, + value, + ]) => !protectedKeys.has(key) && value !== undefined), + ); + const cleanFields = { + ...suppliedNamedFields, + ...safeAdditionalFields, + }; + + if (!this.id) { + const missingRequired = !this.name || !this.description || this.riskScore === undefined + || !this.severity || !this.type; + if (missingRequired) { + throw new ConfigurationError("`name`, `description`, `riskScore`, `severity`, and `type` are required when creating a new rule (no `id` provided)."); + } + const response = await this.elasticSecurity.createDetectionRule({ + $, + data: { + rule_id: this.ruleId, + ...cleanFields, + }, + }); + $.export("$summary", `Created detection rule "${response.name}" (${response.id})`); + return response; + } + + const current = await this.elasticSecurity.getDetectionRule({ + $, + params: { + id: this.id, + }, + }); + if (this.type && this.type !== current.type) { + throw new ConfigurationError(`Cannot change a rule's type from \`${current.type}\` to \`${this.type}\` on update — type is fixed at creation. Delete and recreate the rule if you need a different type.`); + } + const merged = { + ...current, + ...cleanFields, + }; + // Kibana's PUT rejects a body carrying both `id` and `rule_id` — the GET response always + // includes both, so keep only `id` (always present) to identify the rule being updated. + delete merged.rule_id; + for (const field of RULE_READ_ONLY_FIELDS) { + delete merged[field]; + } + const response = await this.elasticSecurity.updateDetectionRule({ + $, + data: merged, + }); + $.export("$summary", `Updated detection rule "${response.name}" (${response.id})`); + return response; + }, +}; diff --git a/components/elastic_security/actions/delete-record/delete-record.mjs b/components/elastic_security/actions/delete-record/delete-record.mjs new file mode 100644 index 0000000000000..619f06ffec3ab --- /dev/null +++ b/components/elastic_security/actions/delete-record/delete-record.mjs @@ -0,0 +1,62 @@ +import elasticSecurity from "../../elastic_security.app.mjs"; + +export default { + key: "elastic_security-delete-record", + name: "Delete Record", + description: "Permanently delete an Elastic Security case or detection rule by ID." + + " Cases are deleted via DELETE /api/cases; detection rules via DELETE /api/detection_engine/rules." + + " Run **Find Cases** or **Find Detection Rules** first to obtain a valid ID for the object you want to delete." + + " Example: calling with `objectType: \"case\"` and `recordId: \"a1c1...\"` returns `{ success: true, objectType: \"case\", recordId: \"a1c1...\" }`." + + " This is destructive and cannot be undone." + + " [See the delete case documentation](https://www.elastic.co/docs/api/doc/kibana/operation/operation-deletecasedefaultspace) and the [delete rule documentation](https://www.elastic.co/docs/api/doc/kibana/operation/operation-deleterule)", + version: "0.0.1", + type: "action", + ai: "optimized", + annotations: { + readOnlyHint: false, + destructiveHint: true, + openWorldHint: true, + }, + props: { + elasticSecurity, + objectType: { + propDefinition: [ + elasticSecurity, + "objectType", + ], + description: "The type of object to delete.", + }, + recordId: { + type: "string", + label: "Record ID", + description: "The ID of the object to delete. For a case, its case ID from **Find Cases** (e.g. `a1c10c9b-8448-483a-81f7-a4b3225eb6b8`). For a detection rule, its Kibana internal UUID from **Find Detection Rules**' `id` field (e.g. `7ac3c66d-f0b4-4f7c-a576-7bb91bf4e9ce`) — not the user-defined `rule_id`.", + }, + }, + async run({ $ }) { + if (this.objectType === "case") { + await this.elasticSecurity.deleteCase({ + $, + params: { + ids: JSON.stringify([ + this.recordId, + ]), + }, + }); + $.export("$summary", `Deleted case ${this.recordId}`); + return { + success: true, + objectType: this.objectType, + recordId: this.recordId, + }; + } + + const response = await this.elasticSecurity.deleteDetectionRule({ + $, + params: { + id: this.recordId, + }, + }); + $.export("$summary", `Deleted detection rule ${this.recordId}`); + return response; + }, +}; diff --git a/components/elastic_security/actions/find-assignable-users/find-assignable-users.mjs b/components/elastic_security/actions/find-assignable-users/find-assignable-users.mjs new file mode 100644 index 0000000000000..b60cac9ae2c0d --- /dev/null +++ b/components/elastic_security/actions/find-assignable-users/find-assignable-users.mjs @@ -0,0 +1,29 @@ +import elasticSecurity from "../../elastic_security.app.mjs"; + +export default { + key: "elastic_security-find-assignable-users", + name: "Find Assignable Users", + description: "List users who have created or reported Elastic Security cases, via GET /api/cases/reporters, to discover valid `profile_uid` values for the `assignees` parameter on **Create or Update Case**." + + " Kibana has no public endpoint for listing every org user or for listing who is eligible for assignment — this endpoint only covers people who have reported at least one case, which is a subset of valid assignees, not the full set." + + " If the person you need doesn't appear here (e.g. they've never reported a case), ask the user for their `profile_uid` directly instead of guessing." + + " Example: calling with no parameters returns `[{ username: \"jsmith\", full_name: \"Jane Smith\", email: \"jane@example.com\", profile_uid: \"u_abc123_cloud\" }]`; pass that `profile_uid` as an entry in **Create or Update Case**'s `assignees` array." + + " [See the documentation](https://www.elastic.co/docs/api/doc/kibana/operation/operation-getcasereportersdefaultspace)", + version: "0.0.1", + type: "action", + ai: "optimized", + annotations: { + readOnlyHint: true, + destructiveHint: false, + openWorldHint: true, + }, + props: { + elasticSecurity, + }, + async run({ $ }) { + const reporters = await this.elasticSecurity.listCaseReporters({ + $, + }); + $.export("$summary", `Found ${reporters.length} user(s) with case history`); + return reporters; + }, +}; diff --git a/components/elastic_security/actions/find-cases/find-cases.mjs b/components/elastic_security/actions/find-cases/find-cases.mjs new file mode 100644 index 0000000000000..91af98d1f9f11 --- /dev/null +++ b/components/elastic_security/actions/find-cases/find-cases.mjs @@ -0,0 +1,126 @@ +import elasticSecurity from "../../elastic_security.app.mjs"; +import { pickFields } from "../../common/utils.mjs"; + +export default { + key: "elastic_security-find-cases", + name: "Find Cases", + description: "Find and list Elastic Security cases via GET /api/cases/_find, or fetch a single case directly via GET /api/cases/{caseId} when `caseId` is provided." + + " Use this to search/browse cases, or to look up one case's full details (including its `version` token) once you have an ID." + + " Run this first to obtain a `caseId` before using **Create or Update Case**, **Add Case Comment**, or **Delete Record**." + + " Example: calling with `search: \"perimeter breach\"` and `status: \"open\"` returns `{ total: 1, cases: [{ id: \"a1c1...\", title: \"Isla Nublar Perimeter Breach\", severity: \"high\", status: \"open\", ... }] }`; use `fields` to shrink each case down to just the fields you need." + + " [See the documentation](https://www.elastic.co/docs/api/doc/kibana/operation/operation-findcasesdefaultspace)", + version: "0.0.1", + type: "action", + ai: "optimized", + annotations: { + readOnlyHint: true, + destructiveHint: false, + openWorldHint: true, + }, + props: { + elasticSecurity, + caseId: { + propDefinition: [ + elasticSecurity, + "caseId", + ], + description: "Fetch a single case directly by ID instead of searching. When provided, all other search/filter parameters are ignored.", + optional: true, + }, + search: { + type: "string", + label: "Search", + description: "Free-text search string to match against case fields. Ignored when `caseId` is provided.", + optional: true, + }, + status: { + propDefinition: [ + elasticSecurity, + "status", + ], + description: "Filter by case status. One of: `open`, `in-progress`, `closed`. Ignored when `caseId` is provided.", + optional: true, + }, + severity: { + propDefinition: [ + elasticSecurity, + "severity", + ], + description: "Filter by case severity. One of: `low`, `medium`, `high`, `critical`. Ignored when `caseId` is provided.", + optional: true, + }, + tags: { + propDefinition: [ + elasticSecurity, + "tags", + ], + description: "Filter by one or more tags. Run **List Tags** first to see existing case tags. Ignored when `caseId` is provided.", + optional: true, + }, + sortField: { + propDefinition: [ + elasticSecurity, + "sortField", + ], + description: "Field to sort by (e.g. `createdAt`, `updatedAt`, `severity`, `status`). Ignored when `caseId` is provided.", + }, + sortOrder: { + propDefinition: [ + elasticSecurity, + "sortOrder", + ], + description: "Sort direction: `asc` or `desc`. Ignored when `caseId` is provided.", + }, + page: { + propDefinition: [ + elasticSecurity, + "page", + ], + }, + perPage: { + propDefinition: [ + elasticSecurity, + "perPage", + ], + }, + fields: { + propDefinition: [ + elasticSecurity, + "fields", + ], + description: "Only include these fields in each returned case, to reduce response size. Omit to return the full case object(s)." + + " Common fields: `id`, `title`, `description`, `severity`, `status`, `tags`, `category`, `assignees`, `created_at`, `updated_at`, `version`, `totalComment`, `totalAlerts`.", + }, + }, + async run({ $ }) { + if (this.caseId) { + const response = await this.elasticSecurity.getCase({ + $, + caseId: this.caseId, + }); + $.export("$summary", `Retrieved case "${response.title}" (${response.id})`); + return pickFields(response, this.fields); + } + const response = await this.elasticSecurity.findCases({ + $, + params: { + search: this.search, + status: this.status, + severity: this.severity, + tags: this.tags, + sortField: this.sortField, + sortOrder: this.sortOrder, + page: this.page, + perPage: this.perPage, + }, + }); + $.export("$summary", `Found ${response.total} case(s)`); + if (this.fields?.length) { + return { + ...response, + cases: response.cases.map((c) => pickFields(c, this.fields)), + }; + } + return response; + }, +}; diff --git a/components/elastic_security/actions/find-detection-rules/find-detection-rules.mjs b/components/elastic_security/actions/find-detection-rules/find-detection-rules.mjs new file mode 100644 index 0000000000000..11da41fdd193f --- /dev/null +++ b/components/elastic_security/actions/find-detection-rules/find-detection-rules.mjs @@ -0,0 +1,114 @@ +import { ConfigurationError } from "@pipedream/platform"; +import elasticSecurity from "../../elastic_security.app.mjs"; +import { pickFields } from "../../common/utils.mjs"; + +export default { + key: "elastic_security-find-detection-rules", + name: "Find Detection Rules", + description: "Find and list Elastic Security detection rules via GET /api/detection_engine/rules/_find, or fetch a single rule directly via GET /api/detection_engine/rules when `id` or `ruleId` is provided." + + " Use this to search/browse rules, or to look up one rule's full definition once you have an ID." + + " Run this first to obtain an `id`/`ruleId` before using **Create or Update Detection Rule**, **Run Detection Rule**, or **Delete Record**." + + " Example: calling with `filter: 'alert.attributes.enabled: true'` returns `{ total: 3, data: [{ id: \"7ac3...\", name: \"InGen Perimeter Query Rule\", type: \"query\", enabled: true, ... }] }`; use `fields` to shrink each rule down to just the fields you need — rule objects carry many advanced fields (`exceptions_list`, `related_integrations`, `threat`, etc.) that are rarely relevant." + + " [See the documentation](https://www.elastic.co/docs/api/doc/kibana/operation/operation-findrules)", + version: "0.0.1", + type: "action", + ai: "optimized", + annotations: { + readOnlyHint: true, + destructiveHint: false, + openWorldHint: true, + }, + props: { + elasticSecurity, + id: { + propDefinition: [ + elasticSecurity, + "id", + ], + description: "Fetch a single rule directly by its Kibana internal UUID instead of searching. Provide either this or `ruleId`, not both. When set, all filter/sort/pagination parameters are ignored.", + optional: true, + }, + ruleId: { + propDefinition: [ + elasticSecurity, + "ruleId", + ], + description: "Fetch a single rule directly by its user-defined `rule_id` instead of searching. Provide either this or `id`, not both. When set, all filter/sort/pagination parameters are ignored.", + optional: true, + }, + filter: { + type: "string", + label: "Filter", + description: "KQL/Lucene filter over rule attributes using the `alert.attributes.` syntax (e.g. `alert.attributes.name: \"My Rule\"` or `alert.attributes.enabled: true`). Ignored when `id`/`ruleId` is provided.", + optional: true, + }, + sortField: { + propDefinition: [ + elasticSecurity, + "sortField", + ], + description: "Field to sort by. One of: `created_at`, `createdAt`, `enabled`, `name`, `risk_score`, `riskScore`, `severity`, `updated_at`, `updatedAt`. Ignored when `id`/`ruleId` is provided.", + }, + sortOrder: { + propDefinition: [ + elasticSecurity, + "sortOrder", + ], + description: "Sort direction: `asc` or `desc`. Ignored when `id`/`ruleId` is provided.", + }, + page: { + propDefinition: [ + elasticSecurity, + "page", + ], + }, + perPage: { + propDefinition: [ + elasticSecurity, + "perPage", + ], + }, + fields: { + propDefinition: [ + elasticSecurity, + "fields", + ], + description: "Only include these fields in each returned rule, to reduce response size. Omit to return the full rule object(s)." + + " Common fields: `id`, `rule_id`, `name`, `description`, `type`, `enabled`, `risk_score`, `severity`, `tags`, `query`, `index`, `interval`, `created_at`, `updated_at`.", + }, + }, + async run({ $ }) { + if (this.id && this.ruleId) { + throw new ConfigurationError("Provide either `id` or `ruleId`, not both."); + } + if (this.id || this.ruleId) { + const response = await this.elasticSecurity.getDetectionRule({ + $, + params: { + id: this.id, + rule_id: this.ruleId, + }, + }); + $.export("$summary", `Retrieved detection rule "${response.name}" (${response.id})`); + return pickFields(response, this.fields); + } + const response = await this.elasticSecurity.findDetectionRules({ + $, + params: { + filter: this.filter, + sort_field: this.sortField, + sort_order: this.sortOrder, + page: this.page, + per_page: this.perPage, + }, + }); + $.export("$summary", `Found ${response.total} detection rule(s)`); + if (this.fields?.length) { + return { + ...response, + data: response.data.map((rule) => pickFields(rule, this.fields)), + }; + } + return response; + }, +}; diff --git a/components/elastic_security/actions/list-tags/list-tags.mjs b/components/elastic_security/actions/list-tags/list-tags.mjs new file mode 100644 index 0000000000000..d1e64cb82a831 --- /dev/null +++ b/components/elastic_security/actions/list-tags/list-tags.mjs @@ -0,0 +1,43 @@ +import elasticSecurity from "../../elastic_security.app.mjs"; + +export default { + key: "elastic_security-list-tags", + name: "List Tags", + description: "List all unique tags currently in use across Elastic Security cases via GET /api/cases/tags, or detection rules via GET /api/detection_engine/tags." + + " Use this before tagging a case or rule so you reuse an existing tag instead of creating a near-duplicate (e.g. `incident-response` vs. `incident_response`)." + + " Cross-referenced by the `tags` parameter on **Create or Update Case**, **Create or Update Detection Rule**, and **Find Cases**." + + " Example: calling with `objectType: \"case\"` returns `[\"council-jurassic-eval\", \"ransomware\", \"insider-threat\"]`." + + " [See the case tags documentation](https://www.elastic.co/docs/api/doc/kibana/operation/operation-getcasetagsdefaultspace) and the [rule tags documentation](https://www.elastic.co/docs/api/doc/kibana/operation/operation-readtags)", + version: "0.0.1", + type: "action", + ai: "optimized", + annotations: { + readOnlyHint: true, + destructiveHint: false, + openWorldHint: true, + }, + props: { + elasticSecurity, + objectType: { + propDefinition: [ + elasticSecurity, + "objectType", + ], + description: "Whether to list tags used on cases or on detection rules.", + }, + }, + async run({ $ }) { + if (this.objectType === "case") { + const tags = await this.elasticSecurity.listCaseTags({ + $, + }); + $.export("$summary", `Found ${tags.length} case tag(s)`); + return tags; + } + const tags = await this.elasticSecurity.listRuleTags({ + $, + }); + $.export("$summary", `Found ${tags.length} detection rule tag(s)`); + return tags; + }, +}; diff --git a/components/elastic_security/actions/run-detection-rule/run-detection-rule.mjs b/components/elastic_security/actions/run-detection-rule/run-detection-rule.mjs new file mode 100644 index 0000000000000..6f94cd324015f --- /dev/null +++ b/components/elastic_security/actions/run-detection-rule/run-detection-rule.mjs @@ -0,0 +1,71 @@ +import elasticSecurity from "../../elastic_security.app.mjs"; +import { BULK_ACTION_RUN } from "../../common/constants.mjs"; +import { getDefaultRunWindow } from "../../common/utils.mjs"; + +export default { + key: "elastic_security-run-detection-rule", + name: "Run Detection Rule", + description: "Manually run one or more Elastic Security detection rules over a time range via POST /api/detection_engine/rules/_bulk_action (bulk action `run`)." + + " Use this to test a rule immediately instead of waiting for its next scheduled interval, or to backfill detections over a past window." + + " Provide the rule ids to execute. Run **Find Detection Rules** first to obtain valid ids." + + " Defaults to roughly the last hour if not specified: `endDate` defaults to one minute ago (a small buffer so clock skew/latency can't push it into the future, which Kibana rejects), and `startDate` defaults to one hour before that." + + " Note: Kibana rejects manual runs against disabled rules — the rule must have `enabled: true` (see **Create or Update Detection Rule**)." + + " Example: calling with `ids: [\"7ac3...\"]` and no dates returns `{ attributes: { results: { created: [{ id: \"7ac3...\", name: \"...\" }] }, summary: { succeeded: 1, failed: 0 } } }`." + + " [See the documentation](https://www.elastic.co/docs/api/doc/kibana/operation/operation-performrulesbulkaction)", + version: "0.0.1", + type: "action", + ai: "optimized", + annotations: { + readOnlyHint: false, + destructiveHint: false, + openWorldHint: true, + }, + props: { + elasticSecurity, + ids: { + type: "string[]", + label: "Rule IDs", + description: "Kibana rule UUIDs to run. At least one required. Run **Find Detection Rules** first to obtain valid IDs.", + }, + startDate: { + type: "string", + label: "Start Date", + description: "Start of the manual run time range as an ISO 8601 timestamp (e.g. `2026-08-27T00:00:00.000Z`). If omitted, defaults to one hour before the resolved `endDate` (i.e. one hour and one minute before now, when `endDate` is also omitted).", + optional: true, + }, + endDate: { + type: "string", + label: "End Date", + description: "End of the manual run time range as an ISO 8601 timestamp (e.g. `2026-08-27T01:00:00.000Z`). Defaults to one minute ago, not the exact current time — this buffer keeps the request from landing in the future on Kibana's server clock.", + optional: true, + }, + }, + async run({ $ }) { + const { + startDate, endDate, + } = getDefaultRunWindow({ + startDate: this.startDate, + endDate: this.endDate, + }); + const response = await this.elasticSecurity.runDetectionRules({ + $, + data: { + action: BULK_ACTION_RUN, + ids: this.ids, + run: { + start_date: startDate, + end_date: endDate, + }, + }, + }); + const succeeded = response?.attributes?.summary?.succeeded; + const failed = response?.attributes?.summary?.failed; + const summary = succeeded === undefined + ? `Manually triggered ${this.ids.length} detection rule(s)` + : `Manually triggered ${succeeded} detection rule(s)${failed + ? `, ${failed} failed` + : ""}`; + $.export("$summary", summary); + return response; + }, +}; diff --git a/components/elastic_security/actions/search-alerts/search-alerts.mjs b/components/elastic_security/actions/search-alerts/search-alerts.mjs new file mode 100644 index 0000000000000..ab8bc35ae4b98 --- /dev/null +++ b/components/elastic_security/actions/search-alerts/search-alerts.mjs @@ -0,0 +1,82 @@ +import elasticSecurity from "../../elastic_security.app.mjs"; + +export default { + key: "elastic_security-search-alerts", + name: "Search Alerts", + description: "Search Elastic Security detection alerts (signals) via POST /api/detection_engine/signals/search using raw Elasticsearch Query DSL." + + " Use this to find alert IDs before running **Update Alert Status**, or to investigate alert volume/details for a case." + + " Returns the raw Elasticsearch search response with a `hits.hits` array; each hit's `_id` is the signal ID and `_source` holds the alert's full ECS document." + + " Example: calling with `query: {\"bool\":{\"filter\":[{\"term\":{\"kibana.alert.workflow_status\":\"open\"}}]}}` and `size: 5` returns `{ hits: { total: { value: 12 }, hits: [{ _id: \"abc123\", _source: { \"@timestamp\": \"...\", \"kibana.alert.workflow_status\": \"open\", \"host.name\": \"...\" } }, ...] } }`." + + " Omit `query` to match all alerts. `_source` always holds the full ECS document; use `fields` to additionally get a compact, array-valued view of just the fields you need (under each hit's `fields` key) without parsing the full document yourself." + + " [See the documentation](https://www.elastic.co/docs/api/doc/kibana/operation/operation-searchalerts)", + version: "0.0.1", + type: "action", + ai: "optimized", + annotations: { + readOnlyHint: true, + destructiveHint: false, + openWorldHint: true, + }, + props: { + elasticSecurity, + query: { + type: "object", + label: "Query", + description: "Elasticsearch Query DSL object. Example: `{\"bool\":{\"filter\":[{\"term\":{\"kibana.alert.workflow_status\":\"open\"}}]}}`. Omit to match all alerts.", + optional: true, + }, + size: { + type: "integer", + label: "Size", + description: "Maximum number of alerts to return per call. Minimum 0. Defaults to 10. To paginate beyond this limit, increase **From** by **Size** on successive calls (e.g. Size=100, From=0 for page 1; From=100 for page 2).", + optional: true, + min: 0, + max: 100, + }, + from: { + type: "integer", + label: "From", + description: "Zero-based offset of the first alert to return, used for pagination. For example, set **Size** to 100 and **From** to 100 to fetch the second page of results. Defaults to 0.", + optional: true, + min: 0, + }, + sort: { + type: "object", + label: "Sort", + description: "Elasticsearch sort clause as an object mapping field name to `asc`/`desc` (or a sort options object). Example: `{\"@timestamp\":\"desc\"}`. Add more keys to sort by multiple fields.", + optional: true, + }, + trackTotalHits: { + type: "boolean", + label: "Track Total Hits", + description: "Whether to return an accurate total hit count instead of a bounded estimate.", + optional: true, + }, + fields: { + propDefinition: [ + elasticSecurity, + "fields", + ], + description: "Request these specific fields via Elasticsearch's native field retrieval, e.g. `[\"@timestamp\", \"kibana.alert.workflow_status\", \"host.name\"]` — returned under each hit's `fields` key (each value as an array), alongside the unchanged full `_source` document. Useful for reading known field values without parsing all of `_source`." + + " Common fields: `@timestamp`, `kibana.alert.workflow_status`, `kibana.alert.rule.name`, `host.name`, `user.name`, `event.category`.", + }, + }, + async run({ $ }) { + const response = await this.elasticSecurity.searchAlerts({ + $, + data: { + query: this.query, + size: this.size, + from: this.from, + sort: this.sort, + track_total_hits: this.trackTotalHits, + fields: this.fields?.length + ? this.fields + : undefined, + }, + }); + const total = response?.hits?.total?.value ?? response?.hits?.hits?.length ?? 0; + $.export("$summary", `Found ${total} alert(s)`); + return response; + }, +}; diff --git a/components/elastic_security/actions/update-alert-status/update-alert-status.mjs b/components/elastic_security/actions/update-alert-status/update-alert-status.mjs new file mode 100644 index 0000000000000..2d775aea2b39d --- /dev/null +++ b/components/elastic_security/actions/update-alert-status/update-alert-status.mjs @@ -0,0 +1,52 @@ +import elasticSecurity from "../../elastic_security.app.mjs"; +import { ALERT_STATUSES } from "../../common/constants.mjs"; + +export default { + key: "elastic_security-update-alert-status", + name: "Update Alert Status", + description: "Set the workflow status of one or more Elastic Security alerts (signals) by ID via POST /api/detection_engine/signals/status." + + " Run **Search Alerts** first to obtain signal IDs." + + " Example: calling with `alertStatus: \"closed\"`, `signalIds: [\"abc123\"]`, `reason: \"false_positive\"` returns `{ updated: 1, version_conflicts: 0 }`." + + " [See the documentation](https://www.elastic.co/docs/api/doc/kibana/operation/operation-setalertsstatus)", + version: "0.0.1", + type: "action", + ai: "optimized", + annotations: { + readOnlyHint: false, + destructiveHint: false, + openWorldHint: true, + }, + props: { + elasticSecurity, + alertStatus: { + type: "string", + label: "Alert Status", + description: "New alert status. One of: `open`, `acknowledged`, `in-progress`, `closed`. This is the alert's own workflow status, distinct from a case's status.", + options: ALERT_STATUSES, + }, + signalIds: { + type: "string[]", + label: "Signal IDs", + description: "Signal (alert) IDs to update; at least one required. Run **Search Alerts** first to obtain IDs (the `_id` field of each hit). Mapped to the API field `signal_ids`.", + }, + reason: { + type: "string", + label: "Reason", + description: "Optional reason for the status change (e.g. `false_positive`, `duplicate`, `true_positive`, `benign_positive`, `automated_closure`, `other`, or a custom string).", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.elasticSecurity.updateAlertStatus({ + $, + data: { + status: this.alertStatus, + signal_ids: this.signalIds, + reason: this.reason, + }, + }); + const updatedCount = response?.updated ?? this.signalIds.length; + $.export("$summary", `Updated ${updatedCount} alert(s) to status "${this.alertStatus}"`); + return response; + }, +}; diff --git a/components/elastic_security/common/constants.mjs b/components/elastic_security/common/constants.mjs new file mode 100644 index 0000000000000..83598e1c669f6 --- /dev/null +++ b/components/elastic_security/common/constants.mjs @@ -0,0 +1,72 @@ +export const BULK_ACTION_RUN = "run"; + +export const ALERT_STATUSES = [ + "open", + "acknowledged", + "in-progress", + "closed", +]; + +export const SEVERITIES = [ + "low", + "medium", + "high", + "critical", +]; + +export const RULE_TYPES = [ + "query", + "eql", + "saved_query", + "threshold", + "threat_match", + "machine_learning", + "new_terms", + "esql", +]; + +export const CASE_STATUSES = [ + "open", + "in-progress", + "closed", +]; + +export const OBJECT_TYPES = [ + "case", + "detection-rule", +]; + +// Fields Kibana returns on a rule read but which are system-managed and rejected on a PUT +// update. `related_integrations`, `required_fields`, and `setup` are NOT in this list — Kibana +// accepts and persists them on PUT, and since PUT is a full replace, omitting them here would +// silently clear any existing values on every update. +export const RULE_READ_ONLY_FIELDS = [ + "created_at", + "created_by", + "updated_at", + "updated_by", + "revision", + "execution_summary", +]; + +export const CASE_OWNER = "securitySolution"; + +export const DEFAULT_CASE_CONNECTOR = { + id: "none", + name: "none", + type: ".none", + fields: null, +}; + +export const CASE_COMMENT_TYPE_USER = "user"; + +export const KBN_XSRF_VALUE = "true"; + +// Minimum for `page` and `perPage`/`per_page` on GET /api/cases/_find and +// GET /api/detection_engine/rules/_find. +export const MIN_LIMIT = 1; + +// GET /api/cases/_find documents a hard cap of 100 on `perPage`. GET +// /api/detection_engine/rules/_find doesn't document a cap, but 100 is enforced +// here too for consistency across both list tools. +export const PER_PAGE_MAX = 100; diff --git a/components/elastic_security/common/utils.mjs b/components/elastic_security/common/utils.mjs new file mode 100644 index 0000000000000..5fa707ec1a3de --- /dev/null +++ b/components/elastic_security/common/utils.mjs @@ -0,0 +1,29 @@ +export function pickFields(obj, fields) { + if (!fields?.length || !obj || typeof obj !== "object") { + return obj; + } + const picked = {}; + for (const field of fields) { + if (Object.hasOwn(obj, field)) { + picked[field] = obj[field]; + } + } + return picked; +} + +// Kibana rejects backfill windows ending in the future; back the default "now" off by a +// minute so request latency and clock skew can't push it past Kibana's server clock. +const RUN_WINDOW_BUFFER_MS = 60 * 1000; +const RUN_WINDOW_DEFAULT_SPAN_MS = 60 * 60 * 1000; + +export function getDefaultRunWindow({ + startDate, endDate, +}) { + const resolvedEndDate = endDate ?? new Date(Date.now() - RUN_WINDOW_BUFFER_MS).toISOString(); + const resolvedStartDate = startDate + ?? new Date(new Date(resolvedEndDate).getTime() - RUN_WINDOW_DEFAULT_SPAN_MS).toISOString(); + return { + startDate: resolvedStartDate, + endDate: resolvedEndDate, + }; +} diff --git a/components/elastic_security/elastic_security.app.mjs b/components/elastic_security/elastic_security.app.mjs index 8775338f08b37..6844c39db15b5 100644 --- a/components/elastic_security/elastic_security.app.mjs +++ b/components/elastic_security/elastic_security.app.mjs @@ -1,11 +1,284 @@ +import { axios } from "@pipedream/platform"; +import { + CASE_OWNER, + KBN_XSRF_VALUE, + MIN_LIMIT, + PER_PAGE_MAX, + SEVERITIES, + CASE_STATUSES, + OBJECT_TYPES, +} from "./common/constants.mjs"; + export default { type: "app", app: "elastic_security", - propDefinitions: {}, + propDefinitions: { + id: { + type: "string", + label: "Rule ID", + description: "The Kibana internal UUID of the detection rule (e.g. `5f8c1a2b-3d4e-5f6a-7b8c-9d0e1f2a3b4c`). Run **Find Detection Rules** first to obtain valid IDs.", + }, + ruleId: { + type: "string", + label: "Rule ID (User-defined)", + description: "The user-defined stable rule identifier (`rule_id`). Run **Find Detection Rules** first to obtain valid `rule_id` values.", + }, + caseId: { + type: "string", + label: "Case ID", + description: "The ID of the case. Run **Find Cases** first to obtain valid case IDs.", + }, + severity: { + type: "string", + label: "Severity", + description: "Severity level. One of: `low`, `medium`, `high`, `critical`.", + options: SEVERITIES, + }, + tags: { + type: "string[]", + label: "Tags", + description: "List of tags. Run **List Tags** first to see existing tags and avoid creating near-duplicates.", + }, + status: { + type: "string", + label: "Status", + description: "Case status. One of: `open`, `in-progress`, `closed`.", + options: CASE_STATUSES, + }, + objectType: { + type: "string", + label: "Object Type", + description: "The type of object: a case or a detection rule.", + options: OBJECT_TYPES, + }, + page: { + type: "integer", + label: "Page", + description: "Page number of results to return, starting at 1. Defaults to 1." + + " If the response's `total` field exceeds `page × perPage`, more results exist — call again with `page` incremented by 1 to fetch them.", + optional: true, + min: MIN_LIMIT, + }, + perPage: { + type: "integer", + label: "Per Page", + description: "Number of results per page. Maximum 100. Defaults to 20. See **Page** for how to fetch additional pages.", + optional: true, + min: MIN_LIMIT, + max: PER_PAGE_MAX, + }, + sortField: { + type: "string", + label: "Sort Field", + description: "Field to sort results by.", + optional: true, + }, + sortOrder: { + type: "string", + label: "Sort Order", + description: "Sort direction: `asc` or `desc`.", + optional: true, + options: [ + "asc", + "desc", + ], + }, + fields: { + type: "string[]", + label: "Fields", + description: "Only include these fields in each returned result, to reduce response size, e.g. `[\"id\", \"name\", \"tags\"]`. Omit to return the full object(s).", + optional: true, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return this.$auth.api_url.replace(/\/$/, ""); + }, + async _makeRequest({ + $ = this, path, headers, method = "GET", ...args + }) { + const xsrfHeaders = method.toUpperCase() !== "GET" + ? { + "kbn-xsrf": KBN_XSRF_VALUE, + } + : {}; + return axios($, { + url: `${this._baseUrl()}${path}`, + method, + headers: { + "authorization": `ApiKey ${this.$auth.api_key}`, + ...xsrfHeaders, + ...headers, + }, + // Kibana rejects array-valued query params serialized as `tags[]=a&tags[]=b` + // (axios's default) with a 400 "invalid keys" error — it expects repeated + // plain keys instead (`tags=a&tags=b`). + paramsSerializer: { + indexes: null, + }, + ...args, + }); + }, + async findDetectionRules({ + $, params, + }) { + return this._makeRequest({ + $, + path: "/api/detection_engine/rules/_find", + params, + }); + }, + async getDetectionRule({ + $, params, + }) { + return this._makeRequest({ + $, + path: "/api/detection_engine/rules", + params, + }); + }, + async createDetectionRule({ + $, data, + }) { + return this._makeRequest({ + $, + method: "POST", + path: "/api/detection_engine/rules", + data, + }); + }, + async updateDetectionRule({ + $, data, + }) { + return this._makeRequest({ + $, + method: "PUT", + path: "/api/detection_engine/rules", + data, + }); + }, + async deleteDetectionRule({ + $, params, + }) { + return this._makeRequest({ + $, + method: "DELETE", + path: "/api/detection_engine/rules", + params, + }); + }, + async runDetectionRules({ + $, data, + }) { + return this._makeRequest({ + $, + method: "POST", + path: "/api/detection_engine/rules/_bulk_action", + data, + }); + }, + async searchAlerts({ + $, data, + }) { + return this._makeRequest({ + $, + method: "POST", + path: "/api/detection_engine/signals/search", + data, + }); + }, + async updateAlertStatus({ + $, data, + }) { + return this._makeRequest({ + $, + method: "POST", + path: "/api/detection_engine/signals/status", + data, + }); + }, + async createCase({ + $, data, + }) { + return this._makeRequest({ + $, + method: "POST", + path: "/api/cases", + data, + }); + }, + async getCase({ + $, caseId, + }) { + return this._makeRequest({ + $, + path: `/api/cases/${encodeURIComponent(caseId)}`, + }); + }, + async findCases({ + $, params, + }) { + return this._makeRequest({ + $, + path: "/api/cases/_find", + params, + }); + }, + async updateCase({ + $, data, + }) { + return this._makeRequest({ + $, + method: "PATCH", + path: "/api/cases", + data, + }); + }, + async deleteCase({ + $, params, + }) { + return this._makeRequest({ + $, + method: "DELETE", + path: "/api/cases", + params, + }); + }, + async addCaseComment({ + $, caseId, data, + }) { + return this._makeRequest({ + $, + method: "POST", + path: `/api/cases/${encodeURIComponent(caseId)}/comments`, + data, + }); + }, + async listCaseTags({ $ }) { + return this._makeRequest({ + $, + path: "/api/cases/tags", + params: { + owner: CASE_OWNER, + }, + }); + }, + // Returns users who have reported (created) cases, not every org user or every + // assignment-eligible user — Kibana has no public endpoint for either of those. + async listCaseReporters({ $ }) { + return this._makeRequest({ + $, + path: "/api/cases/reporters", + params: { + owner: CASE_OWNER, + }, + }); + }, + async listRuleTags({ $ }) { + return this._makeRequest({ + $, + path: "/api/detection_engine/tags", + }); }, }, }; diff --git a/components/elastic_security/package.json b/components/elastic_security/package.json index 1523aa65c3b14..1f61e9fd035a9 100644 --- a/components/elastic_security/package.json +++ b/components/elastic_security/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/elastic_security", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Elastic Security Components", "main": "elastic_security.app.mjs", "keywords": [ @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +}