From 3efeaedbd10c2fc7b3ba7d68aa57d1610edb930e Mon Sep 17 00:00:00 2001 From: Brijesh Khunt Date: Tue, 16 Jun 2026 17:05:59 +0530 Subject: [PATCH 1/2] Add support for the meeting_activity data stream --- packages/zoom/_dev/build/docs/README.md | 127 +++++++- .../_dev/deploy/docker/docker-compose.yml | 13 + .../docker/files/config-meeting_activity.yml | 133 ++++++++ packages/zoom/changelog.yml | 5 + .../_dev/test/pipeline/test-common-config.yml | 3 + .../pipeline/test-zoom-meeting-activity.log | 5 + ...st-zoom-meeting-activity.log-expected.json | 172 +++++++++++ .../_dev/test/system/test-default-config.yml | 20 ++ .../meeting_activity/agent/stream/cel.yml.hbs | 191 ++++++++++++ .../elasticsearch/ingest_pipeline/default.yml | 166 ++++++++++ .../meeting_activity/fields/base-fields.yml | 16 + .../meeting_activity/fields/beats.yml | 6 + .../meeting_activity/fields/fields.yml | 12 + .../data_stream/meeting_activity/manifest.yml | 79 +++++ .../meeting_activity/sample_event.json | 57 ++++ packages/zoom/docs/README.md | 290 +++++++++++++++++- packages/zoom/manifest.yml | 91 +++++- 17 files changed, 1355 insertions(+), 31 deletions(-) create mode 100644 packages/zoom/_dev/deploy/docker/files/config-meeting_activity.yml create mode 100644 packages/zoom/data_stream/meeting_activity/_dev/test/pipeline/test-common-config.yml create mode 100644 packages/zoom/data_stream/meeting_activity/_dev/test/pipeline/test-zoom-meeting-activity.log create mode 100644 packages/zoom/data_stream/meeting_activity/_dev/test/pipeline/test-zoom-meeting-activity.log-expected.json create mode 100644 packages/zoom/data_stream/meeting_activity/_dev/test/system/test-default-config.yml create mode 100644 packages/zoom/data_stream/meeting_activity/agent/stream/cel.yml.hbs create mode 100644 packages/zoom/data_stream/meeting_activity/elasticsearch/ingest_pipeline/default.yml create mode 100644 packages/zoom/data_stream/meeting_activity/fields/base-fields.yml create mode 100644 packages/zoom/data_stream/meeting_activity/fields/beats.yml create mode 100644 packages/zoom/data_stream/meeting_activity/fields/fields.yml create mode 100644 packages/zoom/data_stream/meeting_activity/manifest.yml create mode 100644 packages/zoom/data_stream/meeting_activity/sample_event.json diff --git a/packages/zoom/_dev/build/docs/README.md b/packages/zoom/_dev/build/docs/README.md index 77f046bd610..66176556c0e 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 Rooms events). +- **REST API**: a periodic poll of the Zoom REST API to collect the **meeting activity** logs report for an account. -This integration is compatible with the Zoom Platform API as of September 2020. +### Compatibility + +- The **meeting_activity** data stream uses the Zoom REST API [`GET /report/meeting_activities`](https://developers.zoom.us/docs/api/meetings/#tag/reports/get/report/meeting_activities) endpoint. It is available for Paid or ZMP accounts, and the meeting audit trail log feature must be enabled for the account by Zoom Support. + +### 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 **meeting_activity** data stream periodically queries the Zoom REST API using Server-to-Server OAuth. On each interval it requests meeting activity logs within a date window (a maximum of one month per request), paginates through the results, and advances a cursor so that subsequent runs collect only new activity. + +## 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 Rooms events. +- `meeting_activity`: meeting activity logs from the Zoom REST API reports endpoint, such as a meeting being created or started, a user joining or leaving, in-meeting chat, remote control, and a meeting ending. + +### 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 meeting activity logs report provides an audit trail of meeting lifecycle and participant activity for investigating incidents, monitoring meeting usage, 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:meeting_activity_log:admin` scope) to the app and activate it. The account must be a Paid or ZMP account, and the meeting audit trail log feature must be enabled by Zoom Support. + +## 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**, **Initial Interval** (lookback), and **Activity Type**, 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"}} + +### meeting_activity + +This is the `meeting_activity` data stream. It collects meeting activity logs from the Zoom REST API. + +{{event "meeting_activity"}} + +{{fields "meeting_activity"}} + +### 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: + +- `meeting_activity`: [Get a meeting activities report](https://developers.zoom.us/docs/api/meetings/#tag/reports/get/report/meeting_activities). diff --git a/packages/zoom/_dev/deploy/docker/docker-compose.yml b/packages/zoom/_dev/deploy/docker/docker-compose.yml index 3fb7fade588..25751b79033 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-meeting-activity: + image: docker.elastic.co/observability/stream:v0.20.0 + hostname: zoom-meeting-activity + ports: + - 8090 + volumes: + - ./files:/files:ro + environment: + PORT: '8090' + command: + - http-server + - --addr=:8090 + - --config=/files/config-meeting_activity.yml zoom-webhook-https: image: docker.elastic.co/observability/stream:v0.20.0 volumes: diff --git a/packages/zoom/_dev/deploy/docker/files/config-meeting_activity.yml b/packages/zoom/_dev/deploy/docker/files/config-meeting_activity.yml new file mode 100644 index 00000000000..d9f91c2288d --- /dev/null +++ b/packages/zoom/_dev/deploy/docker/files/config-meeting_activity.yml @@ -0,0 +1,133 @@ +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/meeting_activities + methods: [GET] + query_params: + from: "{from:.*}" + to: "{to:.*}" + activity_type: "{activity_type:.*}" + page_size: "{page_size:.*}" + next_page_token: mock-page-2-token + responses: + - status_code: 200 + headers: + Content-Type: + - "application/json" + body: |- + { + "meeting_activity_logs": [ + { + "meeting_number": "982 610 0285", + "activity_time": "{{ .request.vars.from }} 07:35:12:880", + "operator": "Bob Brown", + "operator_email": "bob@example.com", + "activity_category": "User left", + "activity_detail": "Bob Brown left the meeting" + } + ], + "page_size": 300, + "next_page_token": "mock-page-3-token" + } + # Pagination page 3 — terminal page for round 1. + - path: /v2/report/meeting_activities + methods: [GET] + query_params: + from: "{from:.*}" + to: "{to:.*}" + activity_type: "{activity_type:.*}" + page_size: "{page_size:.*}" + next_page_token: mock-page-3-token + responses: + - status_code: 200 + headers: + Content-Type: + - "application/json" + body: |- + { + "meeting_activity_logs": [ + { + "meeting_number": "982 610 0285", + "activity_time": "{{ .request.vars.from }} 07:40:00:120", + "operator": "Jill Chill", + "operator_email": "jillchill@example.com", + "activity_category": "Meeting ended", + "activity_detail": "Meeting ended" + } + ], + "page_size": 300 + } + # Initial page (round 1) and cursor-resumed page (round 2). + - path: /v2/report/meeting_activities + methods: [GET] + query_params: + from: "{from:.*}" + to: "{to:.*}" + activity_type: "{activity_type:.*}" + page_size: "{page_size:.*}" + responses: + - status_code: 200 + headers: + Content-Type: + - "application/json" + body: |- + {{ if eq .req_num 1 }} + { + "meeting_activity_logs": [ + { + "meeting_number": "982 610 0285", + "activity_time": "{{ .request.vars.from }} 07:09:03:216", + "operator": "Jill Chill", + "operator_email": "jillchill@example.com", + "activity_category": "Meeting Started", + "activity_detail": "Meeting Started" + }, + { + "meeting_number": "982 610 0285", + "activity_time": "{{ .request.vars.from }} 07:09:45:002", + "operator": "Bob Brown", + "operator_email": "bob@example.com", + "activity_category": "User joined", + "activity_detail": "Bob Brown joined the meeting" + } + ], + "page_size": 300, + "next_page_token": "mock-page-2-token" + } + {{ else if eq .req_num 2 }} + { + "meeting_activity_logs": [ + { + "meeting_number": "112 233 4455", + "activity_time": "{{ .request.vars.from }} 09:00:10:500", + "operator": "Alice Adams", + "operator_email": "alice@example.com", + "activity_category": "Meeting created", + "activity_detail": "Meeting created" + } + ], + "page_size": 300 + } + {{ else }} + { + "meeting_activity_logs": [], + "page_size": 300 + } + {{ end }} diff --git a/packages/zoom/changelog.yml b/packages/zoom/changelog.yml index 783498ecf0b..9ee929a9d50 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 `meeting_activity` 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/meeting_activity/_dev/test/pipeline/test-common-config.yml b/packages/zoom/data_stream/meeting_activity/_dev/test/pipeline/test-common-config.yml new file mode 100644 index 00000000000..4da22641654 --- /dev/null +++ b/packages/zoom/data_stream/meeting_activity/_dev/test/pipeline/test-common-config.yml @@ -0,0 +1,3 @@ +fields: + tags: + - preserve_original_event diff --git a/packages/zoom/data_stream/meeting_activity/_dev/test/pipeline/test-zoom-meeting-activity.log b/packages/zoom/data_stream/meeting_activity/_dev/test/pipeline/test-zoom-meeting-activity.log new file mode 100644 index 00000000000..4dfd5ca70ce --- /dev/null +++ b/packages/zoom/data_stream/meeting_activity/_dev/test/pipeline/test-zoom-meeting-activity.log @@ -0,0 +1,5 @@ +{"meeting_number":"982 610 0285","activity_time":"2024-03-21 07:09:03:216","operator":"Jill Chill","operator_email":"jillchill@example.com","activity_category":"Meeting Started","activity_detail":"Meeting Started"} +{"meeting_number":"982 610 0285","activity_time":"2024-03-21 07:09:45:002","operator":"Bob Brown","operator_email":"bob@example.com","activity_category":"User joined","activity_detail":"Bob Brown joined the meeting"} +{"meeting_number":"982 610 0285","activity_time":"2024-03-21 07:35:12:880","operator":"Bob Brown","operator_email":"bob@example.com","activity_category":"User left","activity_detail":"Bob Brown left the meeting"} +{"meeting_number":"982 610 0285","activity_time":"2024-03-21 07:40:00:120","operator":"Jill Chill","operator_email":"jillchill@example.com","activity_category":"Meeting ended","activity_detail":"Meeting ended"} +{"meeting_number":"112 233 4455","activity_time":"2024-03-22 09:00:10:500","operator":"Alice Adams","activity_category":"Meeting created","activity_detail":"Meeting created"} diff --git a/packages/zoom/data_stream/meeting_activity/_dev/test/pipeline/test-zoom-meeting-activity.log-expected.json b/packages/zoom/data_stream/meeting_activity/_dev/test/pipeline/test-zoom-meeting-activity.log-expected.json new file mode 100644 index 00000000000..e2959846d4e --- /dev/null +++ b/packages/zoom/data_stream/meeting_activity/_dev/test/pipeline/test-zoom-meeting-activity.log-expected.json @@ -0,0 +1,172 @@ +{ + "expected": [ + { + "@timestamp": "2024-03-21T07:09:03.216Z", + "ecs": { + "version": "8.11.0" + }, + "event": { + "action": "meeting-started", + "kind": "event", + "original": "{\"meeting_number\":\"982 610 0285\",\"activity_time\":\"2024-03-21 07:09:03:216\",\"operator\":\"Jill Chill\",\"operator_email\":\"jillchill@example.com\",\"activity_category\":\"Meeting Started\",\"activity_detail\":\"Meeting Started\"}", + "type": [ + "info" + ] + }, + "related": { + "user": [ + "jillchill@example.com", + "Jill Chill" + ] + }, + "tags": [ + "preserve_original_event" + ], + "user": { + "email": "jillchill@example.com", + "name": "Jill Chill" + }, + "zoom": { + "meeting_activity": { + "activity_category": "Meeting Started", + "activity_detail": "Meeting Started", + "meeting_number": "982 610 0285" + } + } + }, + { + "@timestamp": "2024-03-21T07:09:45.002Z", + "ecs": { + "version": "8.11.0" + }, + "event": { + "action": "user-joined", + "kind": "event", + "original": "{\"meeting_number\":\"982 610 0285\",\"activity_time\":\"2024-03-21 07:09:45:002\",\"operator\":\"Bob Brown\",\"operator_email\":\"bob@example.com\",\"activity_category\":\"User joined\",\"activity_detail\":\"Bob Brown joined the meeting\"}", + "type": [ + "info" + ] + }, + "related": { + "user": [ + "bob@example.com", + "Bob Brown" + ] + }, + "tags": [ + "preserve_original_event" + ], + "user": { + "email": "bob@example.com", + "name": "Bob Brown" + }, + "zoom": { + "meeting_activity": { + "activity_category": "User joined", + "activity_detail": "Bob Brown joined the meeting", + "meeting_number": "982 610 0285" + } + } + }, + { + "@timestamp": "2024-03-21T07:35:12.880Z", + "ecs": { + "version": "8.11.0" + }, + "event": { + "action": "user-left", + "kind": "event", + "original": "{\"meeting_number\":\"982 610 0285\",\"activity_time\":\"2024-03-21 07:35:12:880\",\"operator\":\"Bob Brown\",\"operator_email\":\"bob@example.com\",\"activity_category\":\"User left\",\"activity_detail\":\"Bob Brown left the meeting\"}", + "type": [ + "info" + ] + }, + "related": { + "user": [ + "bob@example.com", + "Bob Brown" + ] + }, + "tags": [ + "preserve_original_event" + ], + "user": { + "email": "bob@example.com", + "name": "Bob Brown" + }, + "zoom": { + "meeting_activity": { + "activity_category": "User left", + "activity_detail": "Bob Brown left the meeting", + "meeting_number": "982 610 0285" + } + } + }, + { + "@timestamp": "2024-03-21T07:40:00.120Z", + "ecs": { + "version": "8.11.0" + }, + "event": { + "action": "meeting-ended", + "kind": "event", + "original": "{\"meeting_number\":\"982 610 0285\",\"activity_time\":\"2024-03-21 07:40:00:120\",\"operator\":\"Jill Chill\",\"operator_email\":\"jillchill@example.com\",\"activity_category\":\"Meeting ended\",\"activity_detail\":\"Meeting ended\"}", + "type": [ + "info" + ] + }, + "related": { + "user": [ + "jillchill@example.com", + "Jill Chill" + ] + }, + "tags": [ + "preserve_original_event" + ], + "user": { + "email": "jillchill@example.com", + "name": "Jill Chill" + }, + "zoom": { + "meeting_activity": { + "activity_category": "Meeting ended", + "activity_detail": "Meeting ended", + "meeting_number": "982 610 0285" + } + } + }, + { + "@timestamp": "2024-03-22T09:00:10.500Z", + "ecs": { + "version": "8.11.0" + }, + "event": { + "action": "meeting-created", + "kind": "event", + "original": "{\"meeting_number\":\"112 233 4455\",\"activity_time\":\"2024-03-22 09:00:10:500\",\"operator\":\"Alice Adams\",\"activity_category\":\"Meeting created\",\"activity_detail\":\"Meeting created\"}", + "type": [ + "info" + ] + }, + "related": { + "user": [ + "Alice Adams" + ] + }, + "tags": [ + "preserve_original_event" + ], + "user": { + "name": "Alice Adams" + }, + "zoom": { + "meeting_activity": { + "activity_category": "Meeting created", + "activity_detail": "Meeting created", + "meeting_number": "112 233 4455" + } + } + } + ] +} diff --git a/packages/zoom/data_stream/meeting_activity/_dev/test/system/test-default-config.yml b/packages/zoom/data_stream/meeting_activity/_dev/test/system/test-default-config.yml new file mode 100644 index 00000000000..9635aede26d --- /dev/null +++ b/packages/zoom/data_stream/meeting_activity/_dev/test/system/test-default-config.yml @@ -0,0 +1,20 @@ +wait_for_data_timeout: 1m +input: cel +service: zoom-meeting-activity +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 + activity_type: All Activities + page_size: 300 + tags: + - forwarded + - zoom-meeting-activity +assert: + hit_count: 5 diff --git a/packages/zoom/data_stream/meeting_activity/agent/stream/cel.yml.hbs b/packages/zoom/data_stream/meeting_activity/agent/stream/cel.yml.hbs new file mode 100644 index 00000000000..b794758061e --- /dev/null +++ b/packages/zoom/data_stream/meeting_activity/agent/stream/cel.yml.hbs @@ -0,0 +1,191 @@ +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}} + activity_type: {{activity_type}} + want_more: false +redact: + fields: ~ +program: |- + ( + // Workflow overview: Zoom GET /v2/report/meeting_activities returns meeting + // activity audit records for a [from, to] date window (date granularity; max + // 1-month window). The `activity_type` query parameter selects which category + // of activity to collect. 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. The record timestamp `activity_time` is reported as + // "YYYY-MM-DD HH:mm:ss:SSS" (not RFC3339), so only its date prefix is used for + // cursor advancement. + // + // 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 (no retention floor is documented for this report). + // to_date: from_date + 1 month, capped at today (1-month max, no future). + state.from_date.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/meeting_activities?" + { + "from": [from_date], + "to": [to_date], + "activity_type": [string(state.activity_type)], + "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.?meeting_activity_logs.orValue([]).as(logs, + body.?next_page_token.orValue("").as(next_token, + // Latest in-window event date (date prefix of activity_time). + ((size(logs) > 0) ? + logs.map(e, e.activity_time.split(" ")[0]).max() + : + "").as(max_date, + { + // 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 + : + [ + (max_date != "" && max_date >= from_date) ? + (timestamp(max_date + "T00:00:00Z") + 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/meeting_activities: " + ( + (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/meeting_activity/elasticsearch/ingest_pipeline/default.yml b/packages/zoom/data_stream/meeting_activity/elasticsearch/ingest_pipeline/default.yml new file mode 100644 index 00000000000..49cef49c220 --- /dev/null +++ b/packages/zoom/data_stream/meeting_activity/elasticsearch/ingest_pipeline/default.yml @@ -0,0 +1,166 @@ +--- +description: Parse Zoom meeting activity 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.meeting_activity + 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.meeting_activity.* + - date: + field: zoom.meeting_activity.activity_time + target_field: '@timestamp' + formats: + - "yyyy-MM-dd HH:mm:ss:SSS" + - ISO8601 + timezone: UTC + tag: date_meeting_activity_time + if: ctx.zoom?.meeting_activity?.activity_time != null && ctx.zoom.meeting_activity.activity_time != '' + on_failure: + - remove: + field: zoom.meeting_activity.activity_time + ignore_missing: true + tag: remove_meeting_activity_time_on_date_failure + - append: + field: error.message + value: 'Failed to parse zoom.meeting_activity.activity_time: {{{ _ingest.on_failure_message }}}' + tag: append_date_meeting_activity_time_failure + + # event.* + - set: + field: event.kind + tag: set_event_kind + value: event + - set: + field: event.action + tag: set_event_action_from_activity_category + copy_from: zoom.meeting_activity.activity_category + 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.type + tag: append_event_type_info + value: info + + # user.* + - rename: + field: zoom.meeting_activity.operator_email + target_field: user.email + ignore_missing: true + tag: rename_meeting_activity_operator_email_to_user_email + - rename: + field: zoom.meeting_activity.operator + target_field: user.name + ignore_missing: true + tag: rename_meeting_activity_operator_to_user_name + + # related.* + - append: + field: related.user + tag: append_user_email_to_related_user + value: '{{{user.email}}}' + allow_duplicates: false + if: ctx.user?.email != null + - append: + field: related.user + tag: append_user_name_to_related_user + value: '{{{user.name}}}' + allow_duplicates: false + if: ctx.user?.name != null + + # cleanup + - remove: + field: + - zoom.meeting_activity.activity_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/meeting_activity/fields/base-fields.yml b/packages/zoom/data_stream/meeting_activity/fields/base-fields.yml new file mode 100644 index 00000000000..992e993f6d2 --- /dev/null +++ b/packages/zoom/data_stream/meeting_activity/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.meeting_activity +- name: '@timestamp' + external: ecs diff --git a/packages/zoom/data_stream/meeting_activity/fields/beats.yml b/packages/zoom/data_stream/meeting_activity/fields/beats.yml new file mode 100644 index 00000000000..4084f1dc7f5 --- /dev/null +++ b/packages/zoom/data_stream/meeting_activity/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/meeting_activity/fields/fields.yml b/packages/zoom/data_stream/meeting_activity/fields/fields.yml new file mode 100644 index 00000000000..a2b9ad3eb6f --- /dev/null +++ b/packages/zoom/data_stream/meeting_activity/fields/fields.yml @@ -0,0 +1,12 @@ +- name: zoom.meeting_activity + type: group + fields: + - name: activity_category + type: keyword + description: The operator's activity category. + - name: activity_detail + type: keyword + description: The operator's activity detail. + - name: meeting_number + type: keyword + description: The meeting number. diff --git a/packages/zoom/data_stream/meeting_activity/manifest.yml b/packages/zoom/data_stream/meeting_activity/manifest.yml new file mode 100644 index 00000000000..1f380992796 --- /dev/null +++ b/packages/zoom/data_stream/meeting_activity/manifest.yml @@ -0,0 +1,79 @@ +title: Meeting Activity +type: logs +streams: + - input: cel + title: Meeting Activity + description: Collect meeting activity logs report of 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 Meeting Activity logs from Zoom API. Supported units for this parameter are h/m/s. + required: true + show_user: true + default: 720h + - name: activity_type + type: text + title: Activity Type + description: >- + The meeting activity category to collect. Available categories are `All Activities`, `Meeting Created`, `Meeting Started`, `User Join`, `User Left`, `Remote Control`, `In-Meeting Chat` and `Meeting Ended`. + required: true + show_user: true + default: All Activities + - name: page_size + type: integer + title: Page Size + description: Page size for the response of the Zoom API. Maximum is 300. + 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-meeting-activity + - 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/meeting_activity/sample_event.json b/packages/zoom/data_stream/meeting_activity/sample_event.json new file mode 100644 index 00000000000..6c420011913 --- /dev/null +++ b/packages/zoom/data_stream/meeting_activity/sample_event.json @@ -0,0 +1,57 @@ +{ + "@timestamp": "2024-03-21T07:09:03.216Z", + "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.meeting_activity", + "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": "meeting-started", + "agent_id_status": "verified", + "dataset": "zoom.meeting_activity", + "ingested": "2026-06-15T13:03:44Z", + "kind": "event", + "type": [ + "info" + ] + }, + "input": { + "type": "cel" + }, + "related": { + "user": [ + "jillchill@example.com", + "Jill Chill" + ] + }, + "tags": [ + "forwarded", + "zoom-meeting-activity" + ], + "user": { + "email": "jillchill@example.com", + "name": "Jill Chill" + }, + "zoom": { + "meeting_activity": { + "activity_category": "Meeting Started", + "activity_detail": "Meeting Started", + "meeting_number": "982 610 0285" + } + } +} diff --git a/packages/zoom/docs/README.md b/packages/zoom/docs/README.md index 08b8480199e..e8b041479d6 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 Rooms events). +- **REST API**: a periodic poll of the Zoom REST API to collect the **meeting activity** logs report for an account. -This integration is compatible with the Zoom Platform API as of September 2020. +### Compatibility + +- The **meeting_activity** data stream uses the Zoom REST API [`GET /report/meeting_activities`](https://developers.zoom.us/docs/api/meetings/#tag/reports/get/report/meeting_activities) endpoint. It is available for Paid or ZMP accounts, and the meeting audit trail log feature must be enabled for the account by Zoom Support. + +### 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 **meeting_activity** data stream periodically queries the Zoom REST API using Server-to-Server OAuth. On each interval it requests meeting activity logs within a date window (a maximum of one month per request), paginates through the results, and advances a cursor so that subsequent runs collect only new activity. + +## 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 Rooms events. +- `meeting_activity`: meeting activity logs from the Zoom REST API reports endpoint, such as a meeting being created or started, a user joining or leaving, in-meeting chat, remote control, and a meeting ending. + +### 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 meeting activity logs report provides an audit trail of meeting lifecycle and participant activity for investigating incidents, monitoring meeting usage, 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:meeting_activity_log:admin` scope) to the app and activate it. The account must be a Paid or ZMP account, and the meeting audit trail log feature must be enabled by Zoom Support. + +## 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**, **Initial Interval** (lookback), and **Activity Type**, 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,99 @@ 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 | + +### meeting_activity + +This is the `meeting_activity` data stream. It collects meeting activity logs from the Zoom REST API. + +An example event for `meeting_activity` looks as following: + +```json +{ + "@timestamp": "2024-03-21T07:09:03.216Z", + "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.meeting_activity", + "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": "meeting-started", + "agent_id_status": "verified", + "dataset": "zoom.meeting_activity", + "ingested": "2026-06-15T13:03:44Z", + "kind": "event", + "type": [ + "info" + ] + }, + "input": { + "type": "cel" + }, + "related": { + "user": [ + "jillchill@example.com", + "Jill Chill" + ] + }, + "tags": [ + "forwarded", + "zoom-meeting-activity" + ], + "user": { + "email": "jillchill@example.com", + "name": "Jill Chill" + }, + "zoom": { + "meeting_activity": { + "activity_category": "Meeting Started", + "activity_detail": "Meeting Started", + "meeting_number": "982 610 0285" + } + } +} +``` + +**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.meeting_activity.activity_category | The operator's activity category. | keyword | +| zoom.meeting_activity.activity_detail | The operator's activity detail. | keyword | +| zoom.meeting_activity.meeting_number | The meeting number. | 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: + +- `meeting_activity`: [Get a meeting activities report](https://developers.zoom.us/docs/api/meetings/#tag/reports/get/report/meeting_activities). 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 a9723ef1ee5284db6a420ac16913ece4a095d06a Mon Sep 17 00:00:00 2001 From: Brijesh Khunt Date: Tue, 16 Jun 2026 17:09:00 +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 9ee929a9d50..1335dd6d43d 100644 --- a/packages/zoom/changelog.yml +++ b/packages/zoom/changelog.yml @@ -3,7 +3,7 @@ changes: - description: Add support for the `meeting_activity` data stream. type: enhancement - link: https://github.com/elastic/integrations/pull/1 + link: https://github.com/elastic/integrations/pull/19554 - version: "1.23.0" changes: - description: Map `user.email` and `source.ip` from available fields.