From 4749491f5d68daabd6cca3b4e66a25e2ffdc710b Mon Sep 17 00:00:00 2001 From: Brijesh Khunt Date: Mon, 15 Jun 2026 18:39:36 +0530 Subject: [PATCH 1/2] Add support for the operation data stream --- packages/zoom/_dev/build/docs/README.md | 127 +++++++- .../_dev/deploy/docker/docker-compose.yml | 13 + .../deploy/docker/files/config-operation.yml | 135 ++++++++ packages/zoom/changelog.yml | 5 + .../_dev/test/pipeline/test-common-config.yml | 3 + .../test/pipeline/test-zoom-operation.log | 5 + .../test-zoom-operation.log-expected.json | 180 +++++++++++ .../_dev/test/system/test-default-config.yml | 19 ++ .../operation/agent/stream/cel.yml.hbs | 183 +++++++++++ .../elasticsearch/ingest_pipeline/default.yml | 189 +++++++++++ .../operation/fields/base-fields.yml | 16 + .../data_stream/operation/fields/beats.yml | 6 + .../data_stream/operation/fields/fields.yml | 12 + .../zoom/data_stream/operation/manifest.yml | 71 +++++ .../data_stream/operation/sample_event.json | 60 ++++ packages/zoom/docs/README.md | 293 +++++++++++++++++- packages/zoom/manifest.yml | 91 +++++- 17 files changed, 1377 insertions(+), 31 deletions(-) create mode 100644 packages/zoom/_dev/deploy/docker/files/config-operation.yml create mode 100644 packages/zoom/data_stream/operation/_dev/test/pipeline/test-common-config.yml create mode 100644 packages/zoom/data_stream/operation/_dev/test/pipeline/test-zoom-operation.log create mode 100644 packages/zoom/data_stream/operation/_dev/test/pipeline/test-zoom-operation.log-expected.json create mode 100644 packages/zoom/data_stream/operation/_dev/test/system/test-default-config.yml create mode 100644 packages/zoom/data_stream/operation/agent/stream/cel.yml.hbs create mode 100644 packages/zoom/data_stream/operation/elasticsearch/ingest_pipeline/default.yml create mode 100644 packages/zoom/data_stream/operation/fields/base-fields.yml create mode 100644 packages/zoom/data_stream/operation/fields/beats.yml create mode 100644 packages/zoom/data_stream/operation/fields/fields.yml create mode 100644 packages/zoom/data_stream/operation/manifest.yml create mode 100644 packages/zoom/data_stream/operation/sample_event.json diff --git a/packages/zoom/_dev/build/docs/README.md b/packages/zoom/_dev/build/docs/README.md index 77f046bd610..3895881b5ad 100644 --- a/packages/zoom/_dev/build/docs/README.md +++ b/packages/zoom/_dev/build/docs/README.md @@ -1,18 +1,123 @@ -# Zoom Webhook Integration +# Zoom Integration for Elastic -This integration creates an HTTP listener that accepts incoming webhook -callbacks from Zoom. +## Overview -To configure Zoom to send webhooks to this integration, please follow the -[Zoom Documentation](https://developers.zoom.us/docs/api/rest/webhook-only-app). +[Zoom](https://www.zoom.com/) is a unified communications platform that provides meetings, webinars, phone, team chat, and Zoom Rooms. The Zoom integration for Elastic enables you to collect Zoom event and audit data so you can monitor user activity, investigate security incidents, and analyze platform usage in Elastic. -The agent running this integration must be able to accept requests from the -Internet in order for Zoom to be able connect. Zoom requires that the webhook -accept requests over HTTPS. So you must either configure the integration with -a valid TLS certificate or use a reverse proxy in front of the integration. +This integration collects data using two complementary methods: -## Compatibility +- **Webhook**: a real-time HTTP listener that receives event notifications pushed by Zoom (meeting, webinar, recording, user, account, phone, team chat, and Zoom Room events). +- **REST API**: a periodic poll of the Zoom REST API to collect the admin and user **operation** logs report for an account. -This integration is compatible with the Zoom Platform API as of September 2020. +### Compatibility + +- The **operation** data stream uses the Zoom REST API [`GET /report/operationlogs`](https://developers.zoom.us/docs/api/meetings/#tag/reports/get/report/operationlogs) endpoint and requires a Zoom Pro plan or above. + +### How it works + +The **webhook** data stream creates an HTTP listener that accepts incoming webhook callbacks from Zoom. The Elastic Agent running this integration must be reachable from the internet so that Zoom can connect to it. Zoom requires that webhooks are delivered over HTTPS, so you must either configure the integration with a valid TLS certificate or place a reverse proxy that terminates TLS in front of the integration. Incoming events are then routed to the appropriate ingest pipeline based on the Zoom event type. + +The **operation** data stream periodically queries the Zoom REST API using Server-to-Server OAuth. On each interval it requests operation logs within a date window (a maximum of one month per request, within the last six months of available history), paginates through the results, and advances a cursor so that subsequent runs collect only new operations. + +## What data does this integration collect? + +The Zoom integration collects the following data: + +- `webhook`: real-time Zoom event notifications, including account, team chat (channel and message), meeting, phone, recording, user, webinar, and Zoom Room events. +- `operation`: account-wide admin and user operation logs from the Zoom REST API reports endpoint, such as adding a user, changing account settings, or deleting a recording. + +### Supported use cases + +Integrating Zoom with Elastic SIEM provides centralized visibility into collaboration and administrative activity. Webhook events support real-time monitoring and detection across meetings, recordings, users, and administrative changes, while the operation logs report provides an account-wide audit trail of admin and user operations for investigating configuration changes, detecting unauthorized actions, and meeting compliance requirements. + +## What do I need to use this integration? + +### From Zoom + +#### Collecting data via Webhook + +1. Create a Webhook-only app in the [Zoom App Marketplace](https://marketplace.zoom.us/) by following the [Zoom webhook documentation](https://developers.zoom.us/docs/api/webhooks/). +2. Add the event types you want to receive and set the event notification endpoint URL to the public HTTPS address where this integration is reachable. +3. Note the **Secret Token** generated by Zoom. It is used for CRC endpoint validation and to verify the authenticity of incoming events. + +#### Collecting data from the Zoom REST API + +1. Create a **Server-to-Server OAuth** app in the [Zoom App Marketplace](https://marketplace.zoom.us/) by following the [Server-to-Server OAuth documentation](https://developers.zoom.us/docs/internal-apps/s2s-oauth/). +2. Record the app's **Account ID**, **Client ID**, and **Client Secret**. +3. Add the `report:read:admin` scope (or the granular `report:read:operation_logs:admin` scope) to the app and activate it. A Zoom Pro plan or above is required. + +## How do I deploy this integration? + +### Agent-based deployment + +Elastic Agent must be installed. For more details, check the Elastic Agent [installation instructions](docs-content://reference/fleet/install-elastic-agents.md). You can install only one Elastic Agent per host. + +Elastic Agent is required to receive the Zoom webhook callbacks or to poll the Zoom REST API, and to ship the data to Elastic, where the events are then processed via the integration's ingest pipelines. + +### Onboard / configure + +1. In the top search bar in Kibana, search for **Integrations**. +2. In the search bar, type **Zoom**. +3. Select the **Zoom** integration from the search results. +4. Select **Add Zoom** to add the integration. +5. Enable and configure only the collection methods which you will use. + + * To **Collect Zoom logs via Webhook**, you'll need to: + + - Configure the **Listen Address**, **Listen Port**, and **Webhook path** where the integration accepts requests. + - Optionally enable **CRC validation** and provide the **Zoom Secret Token**, and/or configure a custom header to verify incoming requests. + - Provide a valid **TLS** certificate (or front the integration with a TLS-terminating reverse proxy), since Zoom requires HTTPS. + + * To **Collect Zoom logs via REST API**, you'll need to: + + - Configure the **Account ID**, **Client ID**, and **Client Secret** of your Server-to-Server OAuth app. + - Adjust the integration configuration parameters if required, including the **Interval** and **Initial Interval** (lookback), to enable data collection. + +6. Select **Save and continue** to save the integration. + +### Validation + +#### Dashboards populated + +1. In the top search bar in Kibana, search for **Dashboards**. +2. In the search bar, type **Zoom**. +3. Select a dashboard for the dataset you are collecting, and verify the dashboard information is populated. + +## Troubleshooting + +For help with Elastic ingest tools, check [Common problems](https://www.elastic.co/docs/troubleshoot/ingest/fleet/common-problems). + +## Scaling + +For more information on architectures that can be used for scaling this integration, check the [Ingest Architectures](https://www.elastic.co/docs/manage-data/ingest/ingest-reference-architectures) documentation. + +## Reference + +### webhook + +This is the `webhook` data stream. It collects real-time event notifications pushed by Zoom over an HTTP endpoint. + +{{event "webhook"}} {{fields "webhook"}} + +### operation + +This is the `operation` data stream. It collects admin and user operation logs from the Zoom REST API. + +{{event "operation"}} + +{{fields "operation"}} + +### Inputs used + +These inputs are used in this integration: + +- [http_endpoint](https://www.elastic.co/docs/reference/beats/filebeat/filebeat-input-http_endpoint) +- [cel](https://www.elastic.co/docs/reference/beats/filebeat/filebeat-input-cel) + +### API usage + +This integration uses the following APIs: + +- `operation`: [Get operation logs report](https://developers.zoom.us/docs/api/meetings/#tag/reports/get/report/operationlogs). diff --git a/packages/zoom/_dev/deploy/docker/docker-compose.yml b/packages/zoom/_dev/deploy/docker/docker-compose.yml index 3fb7fade588..1e6bc72bbd0 100644 --- a/packages/zoom/_dev/deploy/docker/docker-compose.yml +++ b/packages/zoom/_dev/deploy/docker/docker-compose.yml @@ -10,6 +10,19 @@ services: - STREAM_ADDR=http://elastic-agent:9080/zoom - STREAM_WEBHOOK_HEADER=Authorization=abc123 command: log --start-signal=SIGHUP --delay=5s /sample_logs/account-ndjson.log + zoom-operation: + image: docker.elastic.co/observability/stream:v0.20.0 + hostname: zoom-operation + ports: + - 8090 + volumes: + - ./files:/files:ro + environment: + PORT: '8090' + command: + - http-server + - --addr=:8090 + - --config=/files/config-operation.yml zoom-webhook-https: image: docker.elastic.co/observability/stream:v0.20.0 volumes: diff --git a/packages/zoom/_dev/deploy/docker/files/config-operation.yml b/packages/zoom/_dev/deploy/docker/files/config-operation.yml new file mode 100644 index 00000000000..927f35bbf03 --- /dev/null +++ b/packages/zoom/_dev/deploy/docker/files/config-operation.yml @@ -0,0 +1,135 @@ +rules: + # Zoom S2S OAuth token endpoint (account_credentials grant). + - path: /oauth/token + methods: [POST] + query_params: + grant_type: account_credentials + account_id: test-account-id + request_headers: + Authorization: + - "Basic dGVzdC1jbGllbnQtaWQ6dGVzdC1jbGllbnQtc2VjcmV0" + Content-Type: + - "application/x-www-form-urlencoded" + responses: + - status_code: 200 + headers: + Content-Type: + - "application/json" + body: | + {"access_token":"test-access-token","token_type":"Bearer","expires_in":3600,"scope":"report:read:admin"} + # Pagination page 2 — token from page 1. + - path: /v2/report/operationlogs + methods: [GET] + query_params: + from: "{from:.*}" + to: "{to:.*}" + page_size: "{page_size:.*}" + next_page_token: mock-page-2-token + responses: + - status_code: 200 + headers: + Content-Type: + - "application/json" + body: |- + { + "operation_logs": [ + { + "action": "Delete", + "category_type": "Recording", + "operation_detail": "Delete cloud recording for meeting 11122223333", + "operator": "admin@example.com", + "time": "{{ .request.vars.from }}T14:30:00Z" + } + ], + "from": "{{ .request.vars.from }}", + "to": "{{ .request.vars.to }}", + "page_size": 300, + "next_page_token": "mock-page-3-token" + } + # Pagination page 3 — terminal page for round 1. + - path: /v2/report/operationlogs + methods: [GET] + query_params: + from: "{from:.*}" + to: "{to:.*}" + page_size: "{page_size:.*}" + next_page_token: mock-page-3-token + responses: + - status_code: 200 + headers: + Content-Type: + - "application/json" + body: |- + { + "operation_logs": [ + { + "action": "Update", + "category_type": "Account", + "operation_detail": "Change account setting Cloud Recording from ON to OFF", + "operator": "superadmin@example.com", + "time": "{{ .request.vars.to }}T08:45:00Z" + } + ], + "from": "{{ .request.vars.from }}", + "to": "{{ .request.vars.to }}", + "page_size": 300 + } + # Initial page (round 1) and cursor-resumed page (round 2). + - path: /v2/report/operationlogs + methods: [GET] + query_params: + from: "{from:.*}" + to: "{to:.*}" + page_size: "{page_size:.*}" + responses: + - status_code: 200 + headers: + Content-Type: + - "application/json" + body: |- + {{ if eq .req_num 1 }} + { + "operation_logs": [ + { + "action": "Update", + "category_type": "User", + "operation_detail": "Activate User alice@example.com", + "operator": "admin@example.com", + "time": "{{ .request.vars.from }}T10:00:00Z" + }, + { + "action": "Add", + "category_type": "User", + "operation_detail": "Add User bob@example.com", + "operator": "admin@example.com", + "time": "{{ .request.vars.from }}T11:15:00Z" + } + ], + "from": "{{ .request.vars.from }}", + "to": "{{ .request.vars.to }}", + "page_size": 300, + "next_page_token": "mock-page-2-token" + } + {{ else if eq .req_num 2 }} + { + "operation_logs": [ + { + "action": "Update", + "category_type": "Billing", + "operation_detail": "Update billing contact information", + "operator": "superadmin@example.com", + "time": "{{ .request.vars.from }}T12:00:00Z" + } + ], + "from": "{{ .request.vars.from }}", + "to": "{{ .request.vars.to }}", + "page_size": 300 + } + {{ else }} + { + "operation_logs": [], + "from": "{{ .request.vars.from }}", + "to": "{{ .request.vars.to }}", + "page_size": 300 + } + {{ end }} diff --git a/packages/zoom/changelog.yml b/packages/zoom/changelog.yml index 783498ecf0b..541bc3fd379 100644 --- a/packages/zoom/changelog.yml +++ b/packages/zoom/changelog.yml @@ -1,4 +1,9 @@ # newer versions go on top +- version: "1.24.0" + changes: + - description: Add support for the `operation` data stream. + type: enhancement + link: https://github.com/elastic/integrations/pull/1 - version: "1.23.0" changes: - description: Map `user.email` and `source.ip` from available fields. diff --git a/packages/zoom/data_stream/operation/_dev/test/pipeline/test-common-config.yml b/packages/zoom/data_stream/operation/_dev/test/pipeline/test-common-config.yml new file mode 100644 index 00000000000..4da22641654 --- /dev/null +++ b/packages/zoom/data_stream/operation/_dev/test/pipeline/test-common-config.yml @@ -0,0 +1,3 @@ +fields: + tags: + - preserve_original_event diff --git a/packages/zoom/data_stream/operation/_dev/test/pipeline/test-zoom-operation.log b/packages/zoom/data_stream/operation/_dev/test/pipeline/test-zoom-operation.log new file mode 100644 index 00000000000..2f9e32b87ca --- /dev/null +++ b/packages/zoom/data_stream/operation/_dev/test/pipeline/test-zoom-operation.log @@ -0,0 +1,5 @@ +{"action":"Update","category_type":"User","operation_detail":"Activate User alice@example.com","operator":"admin@example.com","time":"2024-06-07T10:00:00Z"} +{"action":"Add","category_type":"User","operation_detail":"Add User bob@example.com","operator":"admin@example.com","time":"2024-06-07T11:15:00Z"} +{"action":"Delete","category_type":"Recording","operation_detail":"Delete cloud recording for meeting 11122223333","operator":"admin@example.com","time":"2024-06-07T12:30:00Z"} +{"action":"Update","category_type":"Account","operation_detail":"Change account setting Cloud Recording from ON to OFF","operator":"superadmin@example.com","time":"2024-06-08T09:00:00Z"} +{"category_type":"Billing","operation_detail":"Update billing contact information","time":"2024-06-08T10:00:00Z"} diff --git a/packages/zoom/data_stream/operation/_dev/test/pipeline/test-zoom-operation.log-expected.json b/packages/zoom/data_stream/operation/_dev/test/pipeline/test-zoom-operation.log-expected.json new file mode 100644 index 00000000000..170d8e50eb4 --- /dev/null +++ b/packages/zoom/data_stream/operation/_dev/test/pipeline/test-zoom-operation.log-expected.json @@ -0,0 +1,180 @@ +{ + "expected": [ + { + "@timestamp": "2024-06-07T10:00:00.000Z", + "ecs": { + "version": "8.11.0" + }, + "event": { + "action": "update", + "category": [ + "configuration" + ], + "kind": "event", + "original": "{\"action\":\"Update\",\"category_type\":\"User\",\"operation_detail\":\"Activate User alice@example.com\",\"operator\":\"admin@example.com\",\"time\":\"2024-06-07T10:00:00Z\"}", + "type": [ + "info", + "change" + ] + }, + "related": { + "user": [ + "admin@example.com" + ] + }, + "tags": [ + "preserve_original_event" + ], + "user": { + "email": "admin@example.com", + "name": "admin@example.com" + }, + "zoom": { + "operation": { + "action": "Update", + "category_type": "User", + "operation_detail": "Activate User alice@example.com" + } + } + }, + { + "@timestamp": "2024-06-07T11:15:00.000Z", + "ecs": { + "version": "8.11.0" + }, + "event": { + "action": "add", + "category": [ + "configuration" + ], + "kind": "event", + "original": "{\"action\":\"Add\",\"category_type\":\"User\",\"operation_detail\":\"Add User bob@example.com\",\"operator\":\"admin@example.com\",\"time\":\"2024-06-07T11:15:00Z\"}", + "type": [ + "info", + "change", + "creation" + ] + }, + "related": { + "user": [ + "admin@example.com" + ] + }, + "tags": [ + "preserve_original_event" + ], + "user": { + "email": "admin@example.com", + "name": "admin@example.com" + }, + "zoom": { + "operation": { + "action": "Add", + "category_type": "User", + "operation_detail": "Add User bob@example.com" + } + } + }, + { + "@timestamp": "2024-06-07T12:30:00.000Z", + "ecs": { + "version": "8.11.0" + }, + "event": { + "action": "delete", + "category": [ + "configuration" + ], + "kind": "event", + "original": "{\"action\":\"Delete\",\"category_type\":\"Recording\",\"operation_detail\":\"Delete cloud recording for meeting 11122223333\",\"operator\":\"admin@example.com\",\"time\":\"2024-06-07T12:30:00Z\"}", + "type": [ + "info", + "change", + "deletion" + ] + }, + "related": { + "user": [ + "admin@example.com" + ] + }, + "tags": [ + "preserve_original_event" + ], + "user": { + "email": "admin@example.com", + "name": "admin@example.com" + }, + "zoom": { + "operation": { + "action": "Delete", + "category_type": "Recording", + "operation_detail": "Delete cloud recording for meeting 11122223333" + } + } + }, + { + "@timestamp": "2024-06-08T09:00:00.000Z", + "ecs": { + "version": "8.11.0" + }, + "event": { + "action": "update", + "category": [ + "configuration" + ], + "kind": "event", + "original": "{\"action\":\"Update\",\"category_type\":\"Account\",\"operation_detail\":\"Change account setting Cloud Recording from ON to OFF\",\"operator\":\"superadmin@example.com\",\"time\":\"2024-06-08T09:00:00Z\"}", + "type": [ + "info", + "change" + ] + }, + "related": { + "user": [ + "superadmin@example.com" + ] + }, + "tags": [ + "preserve_original_event" + ], + "user": { + "email": "superadmin@example.com", + "name": "superadmin@example.com" + }, + "zoom": { + "operation": { + "action": "Update", + "category_type": "Account", + "operation_detail": "Change account setting Cloud Recording from ON to OFF" + } + } + }, + { + "@timestamp": "2024-06-08T10:00:00.000Z", + "ecs": { + "version": "8.11.0" + }, + "event": { + "category": [ + "configuration" + ], + "kind": "event", + "original": "{\"category_type\":\"Billing\",\"operation_detail\":\"Update billing contact information\",\"time\":\"2024-06-08T10:00:00Z\"}", + "type": [ + "info", + "change" + ] + }, + "tags": [ + "preserve_original_event" + ], + "zoom": { + "operation": { + "category_type": "Billing", + "operation_detail": "Update billing contact information" + } + } + } + ] +} diff --git a/packages/zoom/data_stream/operation/_dev/test/system/test-default-config.yml b/packages/zoom/data_stream/operation/_dev/test/system/test-default-config.yml new file mode 100644 index 00000000000..f14159156df --- /dev/null +++ b/packages/zoom/data_stream/operation/_dev/test/system/test-default-config.yml @@ -0,0 +1,19 @@ +wait_for_data_timeout: 1m +input: cel +service: zoom-operation +vars: + url: http://{{Hostname}}:{{Port}} + token_url: http://{{Hostname}}:{{Port}} + account_id: test-account-id + client_id: test-client-id + client_secret: test-client-secret +data_stream: + vars: + interval: 2s + initial_interval: 24h + page_size: 300 + tags: + - forwarded + - zoom-operation +assert: + hit_count: 5 diff --git a/packages/zoom/data_stream/operation/agent/stream/cel.yml.hbs b/packages/zoom/data_stream/operation/agent/stream/cel.yml.hbs new file mode 100644 index 00000000000..5afac2b3ec5 --- /dev/null +++ b/packages/zoom/data_stream/operation/agent/stream/cel.yml.hbs @@ -0,0 +1,183 @@ +config_version: 2 +interval: {{interval}} +resource.tracer: + enabled: {{enable_request_tracer}} + filename: "../../logs/cel/http-request-trace-*.ndjson" + maxbackups: 5 +{{#if proxy_url}} +resource.proxy_url: {{proxy_url}} +{{/if}} +{{#if ssl}} +resource.ssl: {{ssl}} +{{/if}} +{{#if http_client_timeout}} +resource.timeout: {{http_client_timeout}} +{{/if}} +resource.url: {{url}} +auth.oauth2: + client.id: {{client_id}} + client.secret: {{client_secret}} + token_url: {{token_url}}/oauth/token + endpoint_params: + grant_type: account_credentials + account_id: {{account_id}} +state: + page_size: {{page_size}} + initial_interval: {{initial_interval}} + want_more: false +redact: + fields: ~ +program: |- + ( + // Workflow overview: Zoom GET /v2/report/operationlogs returns admin / user + // operation audit records for a [from, to] date window (date granularity; max + // 1-month window; ~6 months of history). Each poll drains one window + // page-by-page via next_page_token, then advances cursor.poll_start so the next + // interval resumes after the data already collected. Only `cursor` persists + // across runs; `next` holds the in-progress page token within a single poll. + // Dates are "YYYY-MM-DD" strings, which sort chronologically and so can be + // compared and min/max'd directly. + // + // Step 1 - pick the window start. If a page token is in flight we are mid-way + // through a window: keep state as-is so the token drives the next page and + // from_date stays frozen. Otherwise begin the next window (or the first one): + // drop any stale token and seed from_date from the saved cursor, or from + // `now - initial_interval` on the first run. Keying on the token (not want_more) + // lets a finished window advance to the next window while want_more is still + // true for backfill. + (state.?next.page_token.orValue("") != "") ? + state + : + state.drop(["next"]).with( + { + "from_date": state.?cursor.poll_start.orValue( + (now - duration(state.initial_interval)).format("2006-01-02") + ), + } + ) + ).as(state, + // Step 2 - clamp the window. + // from_date: requested start, floored at 6 months ago (Zoom retention). + // to_date: from_date + 1 month, capped at today (1-month max, no future). + [ + state.from_date, + (now - duration("4320h")).format("2006-01-02"), + ].max().as(from_date, + [ + (timestamp(from_date + "T00:00:00Z") + duration("720h")).format("2006-01-02"), + now.format("2006-01-02"), + ].min().as(to_date, + // Step 3 - if the window start already passed the end, everything up to now + // is collected: emit a dropped placeholder (no API call) and hold the cursor. + (from_date > to_date) ? + { + "events": dyn([{"retry": true}]), + "want_more": false, + "cursor": {"poll_start": from_date}, + } + : + // Step 4 - fetch one page of the window. next_page_token is sent only when + // resuming pagination within the same poll. + state.with( + request( + "GET", + state.url.trim_right("/") + "/v2/report/operationlogs?" + { + "from": [from_date], + "to": [to_date], + "page_size": [string(int(state.page_size))], + ?"next_page_token": state.?next.page_token.optFlatMap(v, + (v != "") ? optional.of([v]) : optional.none() + ), + }.format_query() + ).do_request().as(resp, + (resp.StatusCode == 200) ? + resp.Body.decode_json().as(body, + body.?operation_logs.orValue([]).as(logs, + body.?next_page_token.orValue("").as(next_token, + { + // Step 5 - one event per record; emit a dropped placeholder for + // an empty page so want_more / cursor still take effect. + "events": (size(logs) > 0) ? + dyn(logs.map(e, {"message": e.encode_json()})) + : + dyn([{"retry": true}]), + // Keep going while Zoom returns a token (more pages in this + // window) OR the window has not yet reached today (more + // windows to backfill). This drains the whole lookback in one + // polling cycle. + "want_more": next_token != "" || to_date != now.format("2006-01-02"), + // Carry the token within this poll; cleared when the chain ends. + "next": { + ?"page_token": (next_token != "") ? + optional.of(next_token) + : + optional.none(), + }, + // Step 6 - cursor advancement. While still paging, freeze + // poll_start at from_date so the whole window is drained first. + // When the chain ends, advance to the day after the latest + // in-window event (or the day after to_date for an empty + // window), clamped to today so the cursor never lands in the + // future. The candidate is always >= from_date (and from_date + // never regresses), so this cannot move the cursor backwards. + "cursor": { + "poll_start": (next_token != "") ? + from_date + : + [ + (size(logs) > 0 && logs.map(e, e.time).max() >= from_date + "T00:00:00Z") ? + (timestamp(logs.map(e, e.time).max()) + duration("24h")).format("2006-01-02") + : + (timestamp(to_date + "T00:00:00Z") + duration("24h")).format("2006-01-02"), + now.format("2006-01-02"), + ].min(), + }, + } + ) + ) + ) + : + // Non-200: report the error and stop this cycle without advancing the + // cursor (object error form => the agent retries the same window). + // Clear the in-flight page token so the retry restarts the window + // from its first page rather than reusing a stale (possibly expired) + // token. + { + "events": { + "error": { + "code": string(resp.StatusCode), + "id": string(resp.Status), + "message": "GET " + state.url.trim_right("/") + "/v2/report/operationlogs: " + ( + (size(resp.Body) != 0) ? + string(resp.Body) + : + string(resp.Status) + " (" + string(resp.StatusCode) + ")" + ), + }, + }, + "want_more": false, + "next": {}, + } + ) + ) + ) + ) + ) +tags: +{{#if preserve_original_event}} + - preserve_original_event +{{/if}} +{{#each tags as |tag|}} + - {{tag}} +{{/each}} +{{#contains "forwarded" tags}} +publisher_pipeline.disable_host: true +{{/contains}} +processors: + - drop_event: + when: + equals: + retry: true +{{#if processors}} +{{processors}} +{{/if}} diff --git a/packages/zoom/data_stream/operation/elasticsearch/ingest_pipeline/default.yml b/packages/zoom/data_stream/operation/elasticsearch/ingest_pipeline/default.yml new file mode 100644 index 00000000000..a0c17479ab4 --- /dev/null +++ b/packages/zoom/data_stream/operation/elasticsearch/ingest_pipeline/default.yml @@ -0,0 +1,189 @@ +--- +description: Parse Zoom operation logs report events. +processors: + - set: + field: ecs.version + tag: set_ecs_version + value: '8.11.0' + - terminate: + tag: data_collection_error + if: ctx.error?.message != null && ctx.message == null && ctx.event?.original == null + description: error message set and no data to process. + - rename: + field: message + tag: rename_message_to_event_original + target_field: event.original + ignore_missing: true + description: >- + Renames the original `message` field to `event.original` to store a copy of the original message. + The `event.original` field is not touched if the document already has one; it may happen when Logstash sends the document. + if: ctx.event?.original == null + - remove: + field: message + tag: remove_message + ignore_missing: true + description: The `message` field is no longer required if the document has an `event.original` field. + if: ctx.event?.original != null + - json: + field: event.original + target_field: zoom.operation + tag: parse_json_from_event_original + if: ctx.event?.original != null + - fingerprint: + tag: fingerprint_event_original + fields: + - event.original + target_field: _id + ignore_missing: true + + # zoom.operation.* + - date: + field: zoom.operation.time + target_field: '@timestamp' + formats: + - ISO8601 + tag: date_operation_time + if: ctx.zoom?.operation?.time != null && ctx.zoom.operation.time != '' + on_failure: + - remove: + field: zoom.operation.time + ignore_missing: true + tag: remove_operation_time_on_date_failure + - append: + field: error.message + value: 'Failed to parse zoom.operation.time: {{{ _ingest.on_failure_message }}}' + tag: append_date_operation_time_failure + + # event.* + - set: + field: event.kind + tag: set_event_kind + value: event + - set: + field: event.action + tag: set_event_action_from_action + copy_from: zoom.operation.action + ignore_empty_value: true + - lowercase: + field: event.action + tag: lowercase_event_action + ignore_missing: true + - gsub: + field: event.action + pattern: '\s+' + replacement: '-' + tag: gsub_event_action_whitespace + ignore_missing: true + - append: + field: event.category + tag: append_event_category_configuration + value: configuration + - append: + field: event.category + tag: append_event_category_iam + value: iam + if: ctx.zoom?.opeartion?.category_type != null && ['user', 'user_settings', 'account', 'sub_account', 'role'].contains(ctx.zoom.operation.category_type) + - append: + field: event.type + tag: append_event_type_info + value: info + - append: + field: event.type + tag: append_event_type_change + value: change + - append: + field: event.type + tag: append_event_type_creation + value: creation + if: >- + ctx.event?.action != null && ( + ctx.event.action.toLowerCase().contains('create') || + ctx.event.action.toLowerCase().contains('add') + ) + - append: + field: event.type + tag: append_event_type_deletion + value: deletion + if: >- + ctx.event?.action != null && ( + ctx.event.action.toLowerCase().contains('delete') || + ctx.event.action.toLowerCase().contains('remove') + ) + + # user.* + - rename: + field: zoom.operation.operator + target_field: user.email + ignore_missing: true + tag: rename_operation_operator_to_user_email + - set: + field: user.name + tag: set_user_name_from_email + copy_from: user.email + ignore_empty_value: true + + # related.* + - append: + field: related.user + tag: append_user_email_to_related_user + value: '{{{user.email}}}' + allow_duplicates: false + if: ctx.user?.email != null + + # cleanup + - remove: + field: + - zoom.operation.time + ignore_missing: true + tag: remove_fields + - script: + tag: script_to_drop_null_values + lang: painless + description: This script processor iterates over the whole document to remove fields with null values. + source: |- + void handleMap(Map map) { + map.values().removeIf(v -> { + if (v instanceof Map) { + handleMap(v); + } else if (v instanceof List) { + handleList(v); + } + return v == null || v == '' || v == 'N/A' || (v instanceof Map && v.size() == 0) || (v instanceof List && v.size() == 0) + }); + } + void handleList(List list) { + list.removeIf(v -> { + if (v instanceof Map) { + handleMap(v); + } else if (v instanceof List) { + handleList(v); + } + return v == null || v == '' || (v instanceof Map && v.size() == 0) || (v instanceof List && v.size() == 0) + }); + } + handleMap(ctx); + - set: + field: event.kind + tag: set_pipeline_error_into_event_kind + value: pipeline_error + if: ctx.error?.message != null + - append: + field: tags + value: preserve_original_event + allow_duplicates: false + if: ctx.error?.message != null +on_failure: + - append: + field: error.message + value: |- + Processor '{{{ _ingest.on_failure_processor_type }}}' + {{{#_ingest.on_failure_processor_tag}}}with tag '{{{ _ingest.on_failure_processor_tag }}}' + {{{/_ingest.on_failure_processor_tag}}}failed with message '{{{ _ingest.on_failure_message }}}' + - set: + field: event.kind + tag: set_pipeline_error_to_event_kind + value: pipeline_error + - append: + field: tags + value: preserve_original_event + allow_duplicates: false diff --git a/packages/zoom/data_stream/operation/fields/base-fields.yml b/packages/zoom/data_stream/operation/fields/base-fields.yml new file mode 100644 index 00000000000..6f360815648 --- /dev/null +++ b/packages/zoom/data_stream/operation/fields/base-fields.yml @@ -0,0 +1,16 @@ +- name: data_stream.type + external: ecs +- name: data_stream.dataset + external: ecs +- name: data_stream.namespace + external: ecs +- name: event.module + external: ecs + type: constant_keyword + value: zoom +- name: event.dataset + external: ecs + type: constant_keyword + value: zoom.operation +- name: '@timestamp' + external: ecs diff --git a/packages/zoom/data_stream/operation/fields/beats.yml b/packages/zoom/data_stream/operation/fields/beats.yml new file mode 100644 index 00000000000..4084f1dc7f5 --- /dev/null +++ b/packages/zoom/data_stream/operation/fields/beats.yml @@ -0,0 +1,6 @@ +- name: input.type + type: keyword + description: Type of filebeat input. +- name: log.offset + type: long + description: Log offset. diff --git a/packages/zoom/data_stream/operation/fields/fields.yml b/packages/zoom/data_stream/operation/fields/fields.yml new file mode 100644 index 00000000000..a17b8265591 --- /dev/null +++ b/packages/zoom/data_stream/operation/fields/fields.yml @@ -0,0 +1,12 @@ +- name: zoom.operation + type: group + fields: + - name: action + type: keyword + description: The action performed in the operation. + - name: category_type + type: keyword + description: The category of the operation that was performed. + - name: operation_detail + type: keyword + description: A detailed description of the operation that was performed. diff --git a/packages/zoom/data_stream/operation/manifest.yml b/packages/zoom/data_stream/operation/manifest.yml new file mode 100644 index 00000000000..9efb0d0fa63 --- /dev/null +++ b/packages/zoom/data_stream/operation/manifest.yml @@ -0,0 +1,71 @@ +title: Operation +type: logs +streams: + - input: cel + title: Operation + description: Collect operation logs report of admins and users under a Zoom account via the REST API. + template_path: cel.yml.hbs + enabled: false + vars: + - name: interval + type: text + title: Interval + description: Duration between requests to the Zoom API. Supported units for this parameter are h/m/s. + required: true + show_user: true + default: 24h + - name: initial_interval + type: text + title: Initial Interval + description: How far back to pull the Operation logs from Zoom API. Must not exceed six months of available report history. Supported units for this parameter are h/m/s. + required: true + show_user: true + default: 720h + - name: page_size + type: integer + title: Page Size + description: Page size for the response of the Zoom API. + required: true + show_user: false + default: 100 + - name: enable_request_tracer + type: bool + title: Enable request tracing + default: false + multi: false + required: false + show_user: false + description: The request tracer logs requests and responses to the agent's local file-system for debugging configurations. Enabling this request tracing compromises security and should only be used for debugging. See [documentation](https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-input-cel.html#_resource_tracer_enable) for details. + - name: http_client_timeout + type: text + title: HTTP Client Timeout + description: Duration before declaring that the HTTP client connection has timed out. Supported time units are ns, us, ms, s, m, h. + multi: false + required: true + show_user: false + default: 30s + - name: tags + type: text + title: Tags + multi: true + required: true + show_user: false + default: + - forwarded + - zoom-operation + - name: preserve_original_event + required: false + show_user: true + title: Preserve original event + description: Preserves a raw copy of the original event, added to the field `event.original`. + type: bool + multi: false + default: false + - name: processors + type: yaml + title: Processors + multi: false + required: false + show_user: false + description: >- + Processors are used to reduce the number of fields in the exported event or to enhance the event with metadata. This executes in the agent before the logs are parsed. diff --git a/packages/zoom/data_stream/operation/sample_event.json b/packages/zoom/data_stream/operation/sample_event.json new file mode 100644 index 00000000000..a81bb9676d9 --- /dev/null +++ b/packages/zoom/data_stream/operation/sample_event.json @@ -0,0 +1,60 @@ +{ + "@timestamp": "2026-06-14T10:00:00.000Z", + "agent": { + "ephemeral_id": "03b6f4fb-dfde-44c9-87b9-2e42dbf545f7", + "id": "429eb285-b0ae-49a4-84ed-7f45cca62e58", + "name": "elastic-agent-31060", + "type": "filebeat", + "version": "8.19.0" + }, + "data_stream": { + "dataset": "zoom.operation", + "namespace": "62815", + "type": "logs" + }, + "ecs": { + "version": "8.11.0" + }, + "elastic_agent": { + "id": "429eb285-b0ae-49a4-84ed-7f45cca62e58", + "snapshot": false, + "version": "8.19.0" + }, + "event": { + "action": "update", + "agent_id_status": "verified", + "category": [ + "configuration" + ], + "dataset": "zoom.operation", + "ingested": "2026-06-15T13:03:44Z", + "kind": "event", + "type": [ + "info", + "change" + ] + }, + "input": { + "type": "cel" + }, + "related": { + "user": [ + "admin@example.com" + ] + }, + "tags": [ + "forwarded", + "zoom-operation" + ], + "user": { + "email": "admin@example.com", + "name": "admin@example.com" + }, + "zoom": { + "operation": { + "action": "Update", + "category_type": "User", + "operation_detail": "Activate User alice@example.com" + } + } +} diff --git a/packages/zoom/docs/README.md b/packages/zoom/docs/README.md index 08b8480199e..93e7aa03f19 100644 --- a/packages/zoom/docs/README.md +++ b/packages/zoom/docs/README.md @@ -1,19 +1,191 @@ -# Zoom Webhook Integration +# Zoom Integration for Elastic -This integration creates an HTTP listener that accepts incoming webhook -callbacks from Zoom. +## Overview -To configure Zoom to send webhooks to this integration, please follow the -[Zoom Documentation](https://developers.zoom.us/docs/api/rest/webhook-only-app). +[Zoom](https://www.zoom.com/) is a unified communications platform that provides meetings, webinars, phone, team chat, and Zoom Rooms. The Zoom integration for Elastic enables you to collect Zoom event and audit data so you can monitor user activity, investigate security incidents, and analyze platform usage in Elastic. -The agent running this integration must be able to accept requests from the -Internet in order for Zoom to be able connect. Zoom requires that the webhook -accept requests over HTTPS. So you must either configure the integration with -a valid TLS certificate or use a reverse proxy in front of the integration. +This integration collects data using two complementary methods: -## Compatibility +- **Webhook**: a real-time HTTP listener that receives event notifications pushed by Zoom (meeting, webinar, recording, user, account, phone, team chat, and Zoom Room events). +- **REST API**: a periodic poll of the Zoom REST API to collect the admin and user **operation** logs report for an account. -This integration is compatible with the Zoom Platform API as of September 2020. +### Compatibility + +- The **operation** data stream uses the Zoom REST API [`GET /report/operationlogs`](https://developers.zoom.us/docs/api/meetings/#tag/reports/get/report/operationlogs) endpoint and requires a Zoom Pro plan or above. + +### How it works + +The **webhook** data stream creates an HTTP listener that accepts incoming webhook callbacks from Zoom. The Elastic Agent running this integration must be reachable from the internet so that Zoom can connect to it. Zoom requires that webhooks are delivered over HTTPS, so you must either configure the integration with a valid TLS certificate or place a reverse proxy that terminates TLS in front of the integration. Incoming events are then routed to the appropriate ingest pipeline based on the Zoom event type. + +The **operation** data stream periodically queries the Zoom REST API using Server-to-Server OAuth. On each interval it requests operation logs within a date window (a maximum of one month per request, within the last six months of available history), paginates through the results, and advances a cursor so that subsequent runs collect only new operations. + +## What data does this integration collect? + +The Zoom integration collects the following data: + +- `webhook`: real-time Zoom event notifications, including account, team chat (channel and message), meeting, phone, recording, user, webinar, and Zoom Room events. +- `operation`: account-wide admin and user operation logs from the Zoom REST API reports endpoint, such as adding a user, changing account settings, or deleting a recording. + +### Supported use cases + +Integrating Zoom with Elastic SIEM provides centralized visibility into collaboration and administrative activity. Webhook events support real-time monitoring and detection across meetings, recordings, users, and administrative changes, while the operation logs report provides an account-wide audit trail of admin and user operations for investigating configuration changes, detecting unauthorized actions, and meeting compliance requirements. + +## What do I need to use this integration? + +### From Zoom + +#### Collecting data via Webhook + +1. Create a Webhook-only app in the [Zoom App Marketplace](https://marketplace.zoom.us/) by following the [Zoom webhook documentation](https://developers.zoom.us/docs/api/webhooks/). +2. Add the event types you want to receive and set the event notification endpoint URL to the public HTTPS address where this integration is reachable. +3. Note the **Secret Token** generated by Zoom. It is used for CRC endpoint validation and to verify the authenticity of incoming events. + +#### Collecting data from the Zoom REST API + +1. Create a **Server-to-Server OAuth** app in the [Zoom App Marketplace](https://marketplace.zoom.us/) by following the [Server-to-Server OAuth documentation](https://developers.zoom.us/docs/internal-apps/s2s-oauth/). +2. Record the app's **Account ID**, **Client ID**, and **Client Secret**. +3. Add the `report:read:admin` scope (or the granular `report:read:operation_logs:admin` scope) to the app and activate it. A Zoom Pro plan or above is required. + +## How do I deploy this integration? + +### Agent-based deployment + +Elastic Agent must be installed. For more details, check the Elastic Agent [installation instructions](docs-content://reference/fleet/install-elastic-agents.md). You can install only one Elastic Agent per host. + +Elastic Agent is required to receive the Zoom webhook callbacks or to poll the Zoom REST API, and to ship the data to Elastic, where the events are then processed via the integration's ingest pipelines. + +### Onboard / configure + +1. In the top search bar in Kibana, search for **Integrations**. +2. In the search bar, type **Zoom**. +3. Select the **Zoom** integration from the search results. +4. Select **Add Zoom** to add the integration. +5. Enable and configure only the collection methods which you will use. + + * To **Collect Zoom logs via Webhook**, you'll need to: + + - Configure the **Listen Address**, **Listen Port**, and **Webhook path** where the integration accepts requests. + - Optionally enable **CRC validation** and provide the **Zoom Secret Token**, and/or configure a custom header to verify incoming requests. + - Provide a valid **TLS** certificate (or front the integration with a TLS-terminating reverse proxy), since Zoom requires HTTPS. + + * To **Collect Zoom logs via REST API**, you'll need to: + + - Configure the **Account ID**, **Client ID**, and **Client Secret** of your Server-to-Server OAuth app. + - Adjust the integration configuration parameters if required, including the **Interval** and **Initial Interval** (lookback), to enable data collection. + +6. Select **Save and continue** to save the integration. + +### Validation + +#### Dashboards populated + +1. In the top search bar in Kibana, search for **Dashboards**. +2. In the search bar, type **Zoom**. +3. Select a dashboard for the dataset you are collecting, and verify the dashboard information is populated. + +## Troubleshooting + +For help with Elastic ingest tools, check [Common problems](https://www.elastic.co/docs/troubleshoot/ingest/fleet/common-problems). + +## Scaling + +For more information on architectures that can be used for scaling this integration, check the [Ingest Architectures](https://www.elastic.co/docs/manage-data/ingest/ingest-reference-architectures) documentation. + +## Reference + +### webhook + +This is the `webhook` data stream. It collects real-time event notifications pushed by Zoom over an HTTP endpoint. + +An example event for `webhook` looks as following: + +```json +{ + "@timestamp": "2019-07-01T17:03:04.527Z", + "agent": { + "ephemeral_id": "25caa0a1-dfe6-4499-8945-78d3f7b50b5c", + "id": "a2a6d6ce-cd38-4a30-8877-bf698b0d346b", + "name": "docker-fleet-agent", + "type": "filebeat", + "version": "8.8.1" + }, + "data_stream": { + "dataset": "zoom.webhook", + "namespace": "ep", + "type": "logs" + }, + "ecs": { + "version": "8.11.0" + }, + "elastic_agent": { + "id": "a2a6d6ce-cd38-4a30-8877-bf698b0d346b", + "snapshot": false, + "version": "8.8.1" + }, + "event": { + "action": "account.updated", + "agent_id_status": "verified", + "category": [ + "iam" + ], + "dataset": "zoom.webhook", + "ingested": "2023-06-22T16:37:08Z", + "kind": [ + "event" + ], + "original": "{\"event\":\"account.updated\",\"payload\":{\"account_id\":\"abKKcd_IGRCq63yEy673lCA\",\"object\":{\"account_alias\":\"MH\",\"account_name\":\"Michael Harris\",\"id\":\"eFs_EGRCq6ByEyA73qCA\"},\"old_object\":{\"account_alias\":\"\",\"account_name\":\"Mike Harris\",\"id\":\"eFs_EGRCq6ByEyA73qCA\"},\"operator\":\"theoperatoremail@someemail.com\",\"operator_id\":\"iKoRgfbaTazDX6r2Q_eQsQL\",\"time_stamp\":1562000584527}}", + "timezone": "+00:00", + "type": [ + "user", + "change" + ] + }, + "input": { + "type": "http_endpoint" + }, + "observer": { + "product": "Webhook", + "vendor": "Zoom" + }, + "related": { + "user": [ + "iKoRgfbaTazDX6r2Q_eQsQL", + "eFs_EGRCq6ByEyA73qCA" + ] + }, + "tags": [ + "preserve_original_event", + "zoom-webhook", + "forwarded" + ], + "user": { + "changes": { + "full_name": "Michael Harris", + "name": "MH" + }, + "email": "theoperatoremail@someemail.com", + "id": "iKoRgfbaTazDX6r2Q_eQsQL", + "target": { + "full_name": "Mike Harris", + "id": "eFs_EGRCq6ByEyA73qCA" + } + }, + "zoom": { + "account": { + "account_alias": "MH", + "account_name": "Michael Harris" + }, + "master_account_id": "abKKcd_IGRCq63yEy673lCA", + "old_values": { + "account_name": "Mike Harris", + "id": "eFs_EGRCq6ByEyA73qCA" + }, + "operator": "theoperatoremail@someemail.com", + "operator_id": "iKoRgfbaTazDX6r2Q_eQsQL", + "sub_account_id": "eFs_EGRCq6ByEyA73qCA" + } +} +``` **Exported fields** @@ -192,3 +364,102 @@ This integration is compatible with the Zoom Platform API as of September 2020. | zoom.zoomroom.resource_email | Email address associated with the calendar in use by the Zoom room | keyword | | zoom.zoomroom.room_name | The configured name of the Zoom room | keyword | + +### operation + +This is the `operation` data stream. It collects admin and user operation logs from the Zoom REST API. + +An example event for `operation` looks as following: + +```json +{ + "@timestamp": "2026-06-14T10:00:00.000Z", + "agent": { + "ephemeral_id": "03b6f4fb-dfde-44c9-87b9-2e42dbf545f7", + "id": "429eb285-b0ae-49a4-84ed-7f45cca62e58", + "name": "elastic-agent-31060", + "type": "filebeat", + "version": "8.19.0" + }, + "data_stream": { + "dataset": "zoom.operation", + "namespace": "62815", + "type": "logs" + }, + "ecs": { + "version": "8.11.0" + }, + "elastic_agent": { + "id": "429eb285-b0ae-49a4-84ed-7f45cca62e58", + "snapshot": false, + "version": "8.19.0" + }, + "event": { + "action": "update", + "agent_id_status": "verified", + "category": [ + "configuration" + ], + "dataset": "zoom.operation", + "ingested": "2026-06-15T13:03:44Z", + "kind": "event", + "type": [ + "info", + "change" + ] + }, + "input": { + "type": "cel" + }, + "related": { + "user": [ + "admin@example.com" + ] + }, + "tags": [ + "forwarded", + "zoom-operation" + ], + "user": { + "email": "admin@example.com", + "name": "admin@example.com" + }, + "zoom": { + "operation": { + "action": "Update", + "category_type": "User", + "operation_detail": "Activate User alice@example.com" + } + } +} +``` + +**Exported fields** + +| Field | Description | Type | +|---|---|---| +| @timestamp | Date/time when the event originated. This is the date/time extracted from the event, typically representing when the event was generated by the source. If the event source has no original timestamp, this value is typically populated by the first time the event was received by the pipeline. Required field for all events. | date | +| data_stream.dataset | The field can contain anything that makes sense to signify the source of the data. Examples include `nginx.access`, `prometheus`, `endpoint` etc. For data streams that otherwise fit, but that do not have dataset set we use the value "generic" for the dataset value. `event.dataset` should have the same value as `data_stream.dataset`. Beyond the Elasticsearch data stream naming criteria noted above, the `dataset` value has additional restrictions: \* Must not contain `-` \* No longer than 100 characters | constant_keyword | +| data_stream.namespace | A user defined namespace. Namespaces are useful to allow grouping of data. Many users already organize their indices this way, and the data stream naming scheme now provides this best practice as a default. Many users will populate this field with `default`. If no value is used, it falls back to `default`. Beyond the Elasticsearch index naming criteria noted above, `namespace` value has the additional restrictions: \* Must not contain `-` \* No longer than 100 characters | constant_keyword | +| data_stream.type | An overarching type for the data stream. Currently allowed values are "logs" and "metrics". We expect to also add "traces" and "synthetics" in the near future. | constant_keyword | +| event.dataset | Name of the dataset. If an event source publishes more than one type of log or events (e.g. access log, error log), the dataset is used to specify which one the event comes from. It's recommended but not required to start the dataset name with the module name, followed by a dot, then the dataset name. | constant_keyword | +| event.module | Name of the module this data is coming from. If your monitoring agent supports the concept of modules or plugins to process events of a given source (e.g. Apache logs), `event.module` should contain the name of this module. | constant_keyword | +| input.type | Type of filebeat input. | keyword | +| log.offset | Log offset. | long | +| zoom.operation.action | The action performed in the operation. | keyword | +| zoom.operation.category_type | The category of the operation that was performed. | keyword | +| zoom.operation.operation_detail | A detailed description of the operation that was performed. | keyword | + + +### Inputs used + +These inputs are used in this integration: + +- [http_endpoint](https://www.elastic.co/docs/reference/beats/filebeat/filebeat-input-http_endpoint) +- [cel](https://www.elastic.co/docs/reference/beats/filebeat/filebeat-input-cel) + +### API usage + +This integration uses the following APIs: + +- `operation`: [Get operation logs report](https://developers.zoom.us/docs/api/meetings/#tag/reports/get/report/operationlogs). diff --git a/packages/zoom/manifest.yml b/packages/zoom/manifest.yml index 0fea090d6bd..5eff18cca14 100644 --- a/packages/zoom/manifest.yml +++ b/packages/zoom/manifest.yml @@ -1,17 +1,22 @@ name: zoom title: Zoom -version: "1.23.0" +version: "1.24.0" description: Collect logs from Zoom with Elastic Agent. type: integration -format_version: "3.0.2" -categories: +format_version: "3.3.2" +categories: - security - productivity_security # Added observability category as Zoom provides meeting and user activity data for monitoring - observability conditions: kibana: - version: "^8.13.0 || ^9.0.0" + version: "^8.19.0 || ^9.1.0" +icons: + - src: /img/zoom_blue.svg + title: Zoom + size: 516x240 + type: image/svg+xml policy_templates: - name: zoom title: Zoom logs @@ -20,11 +25,79 @@ policy_templates: - type: http_endpoint title: "Collect Zoom logs via Webhook" description: "Collecting logs from Zoom instances via Webhook" + - type: cel + title: "Collect Zoom logs via REST API" + description: "Collecting logs from Zoom instances via the REST API" + vars: + - name: url + type: text + title: URL + description: Base URL of the Zoom API. + required: true + show_user: false + default: https://api.zoom.us + - name: token_url + type: text + title: OAuth Token URL + description: OAuth Token URL of the Zoom OAuth app. + required: true + show_user: false + default: https://zoom.us + - name: account_id + type: text + title: Account ID + description: Account ID of the Zoom account. + required: true + show_user: true + - name: client_id + type: text + title: Client ID + description: Client ID of the Zoom OAuth app. + required: true + show_user: true + - name: client_secret + type: password + title: Client Secret + description: Client secret of the Zoom OAuth app. + required: true + show_user: true + secret: true + - name: proxy_url + type: text + title: Proxy URL + multi: false + required: false + show_user: false + description: URL to proxy connections in the form of http[s]://:@:. Please ensure your username and password are in URL encoded format. + - name: ssl + type: yaml + title: SSL Configuration + description: SSL configuration options. See [documentation](https://www.elastic.co/guide/en/beats/filebeat/current/configuration-ssl.html#ssl-common-config) for details. + multi: false + required: false + show_user: false + default: | + #certificate_authorities: + # - | + # -----BEGIN CERTIFICATE----- + # MIIDCjCCAfKgAwIBAgITJ706Mu2wJlKckpIvkWxEHvEyijANBgkqhkiG9w0BAQsF + # ADAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwIBcNMTkwNzIyMTkyOTA0WhgPMjExOTA2 + # MjgxOTI5MDRaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEB + # BQADggEPADCCAQoCggEBANce58Y/JykI58iyOXpxGfw0/gMvF0hUQAcUrSMxEO6n + # fZRA49b4OV4SwWmA3395uL2eB2NB8y8qdQ9muXUdPBWE4l9rMZ6gmfu90N5B5uEl + # 94NcfBfYOKi1fJQ9i7WKhTjlRkMCgBkWPkUokvBZFRt8RtF7zI77BSEorHGQCk9t + # /D7BS0GJyfVEhftbWcFEAG3VRcoMhF7kUzYwp+qESoriFRYLeDWv68ZOvG7eoWnP + # PsvZStEVEimjvK5NSESEQa9xWyJOmlOKXhkdymtcUd/nXnx6UTCFgnkgzSdTWV41 + # CI6B6aJ9svCTI2QuoIq2HxX/ix7OvW1huVmcyHVxyUECAwEAAaNTMFEwHQYDVR0O + # BBYEFPwN1OceFGm9v6ux8G+DZ3TUDYxqMB8GA1UdIwQYMBaAFPwN1OceFGm9v6ux + # 8G+DZ3TUDYxqMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAG5D + # 874A4YI7YUwOVsVAdbWtgp1d0zKcPRR+r2OdSbTAV5/gcS3jgBJ3i1BN34JuDVFw + # 3DeJSYT3nxy2Y56lLnxDeF8CUTUtVQx3CuGkRg1ouGAHpO/6OqOhwLLorEmxi7tA + # H2O8mtT0poX5AnOAhzVy7QW0D/k4WaoLyckM5hUa6RtvgvLxOwA0U+VGurCDoctu + # 8F4QOgTAWyh8EZIwaKCliFRSynDpv3JTUwtfZkxo6K6nce1RhCWFAsMvDZL8Dgc0 + # yvgJ38BRsFOtkRuAGSf6ZUwTO8JJRRIFnpUzXflAnGivK9M13D5GEQMmIl6U9Pvk + # sxSmbIUfc2SGJGCJD4I= + # -----END CERTIFICATE----- owner: github: elastic/security-service-integrations type: elastic -icons: - - src: /img/zoom_blue.svg - title: Zoom - size: 516x240 - type: image/svg+xml From c3e5f61150941a6c7c82c7335b493577ac853127 Mon Sep 17 00:00:00 2001 From: Brijesh Khunt Date: Mon, 15 Jun 2026 18:42:52 +0530 Subject: [PATCH 2/2] update changelog entry --- packages/zoom/changelog.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/zoom/changelog.yml b/packages/zoom/changelog.yml index 541bc3fd379..4d308e50975 100644 --- a/packages/zoom/changelog.yml +++ b/packages/zoom/changelog.yml @@ -3,7 +3,7 @@ changes: - description: Add support for the `operation` data stream. type: enhancement - link: https://github.com/elastic/integrations/pull/1 + link: https://github.com/elastic/integrations/pull/19523 - version: "1.23.0" changes: - description: Map `user.email` and `source.ip` from available fields.