Google Chat Hardening Guide
Security hardening for Google Chat — app & webhook controls, external chat & spaces, file sharing, history, retention & auto-deletion, third-party archiving, DLP for Chat, space access defaults and space inventory, audit logging, content protection, moderation, and Policy API drift detection.
Overview
Google Chat is the messaging surface of Google Workspace, and an increasingly common path for data exfiltration, phishing, and malware delivery that is monitored less rigorously than email. This guide hardens Chat-specific surfaces: which apps and webhooks can run inside conversations, whether users can chat or share spaces externally, file-sharing posture, history/retention for traceability, and the audit + content-reporting controls that turn Chat into a detection sensor.
This is a product guide within the Google Workspace platform. Platform-wide controls (authentication, OAuth app allowlisting, DLP engine, admin audit logging) live in the Google Workspace Common Controls hub and are referenced here rather than duplicated.
What changed for automation: Chat settings have historically been ClickOps-only, and much published guidance still says so. That is now half-true. The Cloud Identity Policy API exposes eight Chat settings for reading —
chat.chat_apps_access,chat.external_chat_restriction,chat.external_spaces,chat.chat_file_sharing,chat.chat_history,chat.space_history,chat.space_access_default, andchat.third_party_archiving— so almost every control in this guide can now be continuously verified in code even though none can be set in code (Mutate supported: Nofor all of them). The practical consequence runs through the whole guide: configure in the console, then prove and monitor with 3.4. Code Packs on each control follow that split, and say plainly which half they are.
Intended Audience
- Security engineers managing Google Workspace / Google Chat
- IT administrators configuring Admin Console Chat settings
- GRC professionals assessing collaboration-tool compliance (CISA SCuBA, SOC 2)
- Incident responders monitoring messaging-based exfiltration
How to Use This Guide
- L1 (Crawl): Essential controls for all organizations
- L2 (Walk): Enhanced controls for security-sensitive environments
- L3 (Run): Strictest controls for regulated industries
Scope
This guide covers Google Chat hardening in the Google Workspace Admin Console: Chat app & webhook installation controls, external chat/spaces/group-DM restrictions, Chat file-sharing posture, history plus Vault-based retention and auto-deletion, third-party archiving, Chat-scoped DLP rules, space access defaults and org-wide space inventory, and Chat audit logging, content protection, moderation, and configuration drift detection. Platform-wide authentication, OAuth allowlisting, the DLP engine itself, and admin-console audit logging are covered in the Google Workspace guide. Gmail and Drive are covered in their own product guides.
Table of Contents
Controls in this guide
| # | Control | Level | Automatable? |
|---|---|---|---|
| 1.1 | Restrict & allowlist Chat apps | L1 | Verify (Policy API) |
| 2.1 | Restrict external chat & spaces | L1 | Verify (Policy API) |
| 2.2 | Restrict Chat file sharing | L2 | Verify (Policy API) |
| 2.3 | Enforce history & retention | L2 | Verify (Policy API) + enforce holds (Vault API) |
| 2.4 | Auto-deletion retention | L2 | ClickOps only |
| 2.5 | DLP for Chat | L2 | ClickOps only |
| 2.6 | Restricted space access default | L2 | Verify (Policy API) + inventory (Chat API) |
| 2.7 | Govern third-party archiving | L2 | Verify (Policy API) |
| 3.1 | Audit logging & content reporting | L1 | Enforce + detect (Reports API, BigQuery) |
| 3.2 | Content protection coverage limits | L1 | ClickOps only |
| 3.3 | Scoped Chat moderator role | L2 | Enforce (Directory API) |
| 3.4 | Policy API drift detection | L2 | Enforce (Policy API) |
1. App & Integration Security
1.1 Restrict & Allowlist Google Chat Apps
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 2.5, 2.7 |
| NIST 800-53 | AC-3, CM-7 |
| CIS Google Workspace | 2.1 |
Description
Control which Google Chat apps (bots) and incoming webhooks users can add. Disable open installation, require an admin-curated Google Workspace Marketplace allowlist, and restrict incoming webhooks—each of which can read, post, and exfiltrate conversation content programmatically.
Rationale
Why This Matters:
- Chat apps and webhooks run with delegated access to conversations and can silently forward messages or files to external endpoints
- A malicious or over-permissioned Chat app is an OAuth-style data-exfiltration path that bypasses Drive/Gmail controls
- An incoming webhook is authenticated solely by the secret embedded in its URL — the documented form is
https://chat.googleapis.com/v1/spaces/SPACE_ID/messages?key=KEY&token=TOKEN, with no signature verification or separate credential exchange. Anyone who obtains that URL can post into the space as the webhook, which is why a webhook leaked into a repository, a CI log, or a screenshot is a standing phishing channel inside a trusted space rather than a low-severity secret
Attack Prevented: Malicious Chat app installation, webhook abuse, data exfiltration via bot integrations
Prerequisites
- Inventory of currently used Chat apps and webhooks
- Business justification and owner for each approved app
- Marketplace allowlist workflow (see Google Workspace OAuth App Allowlisting)
ClickOps Implementation
Step 1: Restrict Chat App Installation
- Navigate to: Admin Console → Apps → Google Workspace → Google Chat → Chat apps
- Set Allow users to install Chat apps to Off (or leave On only if paired with a Marketplace allowlist)
- Set Allow users to add and use incoming webhooks to Off for the organization (enable only for a dedicated, audited OU if needed)
- Click Save
Note: Chat apps must stay enabled at the top organizational unit for the Chat API to function. Use the Marketplace allowlist—not an OU block—to restrict which apps are usable.
Two documented ways this control is bypassed — plan for both:
- Marketplace overrides the Chat-specific toggle. If Marketplace app installation is enabled, users can install an allowed app even without the Chat-specific install permission. Turning “Allow users to install Chat apps” off is therefore not sufficient on its own — the Marketplace setting is the binding one, which is why Step 2 is not optional.
- Allowlists do not cover unpublished apps. A developer can publish an app to a small number of users without going through Marketplace approval, even with allowlisting enabled. That path is invisible to an allowlist review, so pair the allowlist with detection: monitor
app_addedandapp_invokedin the Chat audit log (3.1) and reconcile installed apps against the approved list rather than assuming the allowlist enforced itself.
Step 2: Curate the Marketplace Allowlist
- Navigate to: Admin Console → Apps → Google Workspace Marketplace apps → Apps list
- Click Google Workspace Marketplace allowlist → Add app to allowlist
- Add only reviewed, business-justified Chat apps
- Set the Marketplace settings so users can install allowlisted apps only
Time to Complete: ~30 minutes
Code Implementation
Code Pack: Terraform
#
# ⚠ PROVIDER STATUS (verified 2026-08-12): the hashicorp/googleworkspace provider
# was ARCHIVED by HashiCorp on 2025-06-30 and is read-only — "New releases will
# not be published." Existing binaries remain downloadable from the registry, so
# this configuration still applies, but it receives no fixes or API updates.
# Pin the version you validated, and treat a fork or a migration to API-based
# automation as a planned action rather than an emergency one.
# https://github.com/hashicorp/terraform-provider-googleworkspace
#
# The provider has never offered ANY Chat-specific resource (its full resource
# set is: chrome_policy, domain, domain_alias, gmail_send_as_alias, group,
# group_member, group_members, group_settings, org_unit, role, role_assignment,
# schema, user), which is why the blocks below build supporting structure only.
#
# Google Chat app installation is governed by two Admin Console settings that
# the googleworkspace provider does NOT expose directly:
#
# 1. Apps > Google Workspace > Google Chat > Chat apps
# - "Allow users to install Chat apps" (On/Off)
# - "Allow users to add and use incoming webhooks" (On/Off)
# 2. Apps > Google Workspace Marketplace apps > Apps list >
# Google Workspace Marketplace allowlist ("Add app to allowlist")
#
# This file creates the governance infrastructure that supports an allowlist
# workflow: a group whose members review and approve Chat app requests.
# Group that owns the Chat app review/approval workflow.
resource "googleworkspace_group" "chat_app_approvers" {
email = "chat-app-approvers@${var.primary_domain}"
name = "Chat App Approvers"
description = "HTH 1.1 -- Members review and approve Google Chat app + webhook requests before they are added to the Marketplace allowlist"
}
# OU for users permitted to add incoming webhooks (L2: restrict webhooks to a
# small, audited population rather than the whole organization).
resource "googleworkspace_org_unit" "chat_webhooks_allowed" {
count = var.profile_level >= 2 ? 1 : 0
name = "Chat Webhooks Allowed"
description = "HTH 1.1 L2 -- Only users in this OU may add incoming webhooks in Chat; disable webhooks for the parent OU"
parent_org_unit_path = var.target_org_unit_path
}
Code Pack: API Script
# Read the live Chat app/webhook policy for the customer. The setting type is
# `chat.chat_apps_access`, with two boolean fields:
# enable_apps -> "Allow users to install Chat apps"
# enable_webhooks -> "Allow users to add and use incoming webhooks"
curl -s -G "https://cloudidentity.googleapis.com/v1/policies" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
--data-urlencode 'filter=setting.type.matches("chat.chat_apps_access")' \
| python3 -c '
import json, sys
for p in json.load(sys.stdin).get("policies", []):
v = p.get("setting", {}).get("value", {})
target = p.get("policyQuery", {}).get("orgUnit", "(customer default)")
apps, hooks = v.get("enable_apps"), v.get("enable_webhooks")
print(f"orgUnit={target} enable_apps={apps} enable_webhooks={hooks}")
if apps: print(" FINDING: users may install Chat apps - require a Marketplace allowlist")
if hooks: print(" FINDING: incoming webhooks enabled - scope to an audited OU only")
'
Code Pack: DB Query
-- Chat app installations. Control 1.1 restricts WHO may install apps; this
-- shows whether the restriction is actually holding, and surfaces installs that
-- predate it. Investigate any actor outside the approved Marketplace workflow.
SELECT
TIMESTAMP_MICROS(time_usec) AS event_time,
email AS actor,
event_name,
ip_address
FROM `project.dataset.activity`
WHERE event_name IN ('app_added', 'app_removed')
AND _PARTITIONTIME >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
AND TIMESTAMP_MICROS(time_usec) >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
ORDER BY event_time DESC;
-- Chat apps run with delegated access to conversation content, so an app being
-- invoked far more than its peers is worth a look — either it is doing more than
-- its business purpose requires, or it is being driven programmatically.
SELECT
email AS actor,
COUNT(*) AS invocations,
COUNT(DISTINCT DATE(TIMESTAMP_MICROS(time_usec))) AS active_days,
COUNT(DISTINCT ip_address) AS distinct_source_ips
FROM `project.dataset.activity`
WHERE event_name = 'app_invoked'
AND _PARTITIONTIME >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
AND TIMESTAMP_MICROS(time_usec) >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
GROUP BY actor
HAVING invocations > 500
ORDER BY invocations DESC;
-- Event PARAMETERS (actor_type, conversation_ownership, conversation_type, and
-- the attachment_* fields) are documented in the Reports API appendix, but their
-- BigQuery column paths are not published. Rather than guessing a path, read the
-- schema your own export actually produced, then extend these queries from it.
SELECT column_name, data_type
FROM `project.dataset.INFORMATION_SCHEMA.COLUMNS`
WHERE table_name = 'activity'
AND LOWER(column_name) LIKE '%chat%'
ORDER BY column_name;
Validation & Testing
- As a standard user, confirm a non-allowlisted Chat app cannot be installed
- Confirm incoming webhook creation is blocked outside the approved OU
- Review Reporting → Audit and investigation → Chat log events for app-related activity
Expected result: Only allowlisted Chat apps are usable; webhooks limited to approved users.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.1 | Logical access security |
| NIST 800-53 | CM-7 | Least functionality |
| CIS Google Workspace | 2.1 | Control third-party apps and add-ons |
2. Data Security
2.1 Restrict External Google Chat & Spaces
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 3.3 |
| NIST 800-53 | AC-3, AC-20 |
| CISA SCuBA | GWS.CHAT.4.1v1 |
Description
Restrict Google Chat and spaces with people outside your organization. Either turn external chat off, or—if external collaboration is required—allow it only for allowlisted (trusted) domains, and apply the same restriction to externally shared spaces and group direct messages.
Rationale
Why This Matters:
- Unrestricted external chat is a low-friction data-exfiltration channel that is monitored less rigorously than email
- “Auto-accept chat invites from familiar contacts” can pull users into external conversations without an explicit decision
- External spaces let outside members persist in a shared room with access to its files and history
- The same setting governs external group direct messages — a distinct exfiltration channel from spaces, and one that is easy to overlook because it creates no persistent room to audit
Attack Prevented: Data exfiltration over Chat, social engineering via external messaging, unauthorized external collaboration
Real-World Incidents:
- Messaging apps are an increasingly common exfiltration path (MITRE ATT&CK T1213.005, Data from Information Repositories: Messaging Applications)
Prerequisites
- Defined list of trusted external domains
- The shared Workspace Allowlisted domains list configured (Account → Domains)
ClickOps Implementation
Step 1: Restrict External Chat
- Navigate to: Admin Console → Apps → Google Workspace → Google Chat → External chat settings
- For Allow users to send messages outside your organization (a.k.a. Chat externally):
- To disable entirely: select Off
- To allow trusted partners only: select On, then check Only allow this for allowlisted domains
- Uncheck Auto-accept chat invites from familiar contacts (L2+)
- Click Save
Step 2: Restrict External Spaces and Group Direct Messages
- Stay on the same page — Admin Console → Apps → Google Workspace → Google Chat → External chat settings. There is no separate “External spaces” page; the spaces and group-DM setting lives alongside the external-chat setting.
- For Allow users to create & join spaces & group direct messages with people outside their organization: select Off, or On restricted to allowlisted domains
- Click Save
⚠ This setting is organization-wide and cannot be scoped per organizational unit. Google’s documentation states plainly: “This setting applies to your entire organization.” The external chat setting in Step 1 can be scoped to an OU (“To apply the setting to a department or team, at the side, select an organizational unit”) — the spaces and group-DM setting cannot. The practical consequence is that the usual “enable it only for the partner-facing OU” pattern is unavailable here: enabling external spaces for one team enables multi-party external DMs for everyone. Treat it as a whole-tenant decision, and use the allowlisted-domains restriction rather than OU scoping to bound it.
Turning it off does not undo what already exists. Per Google’s documentation, disabling external chat “does not delete already created chat messages, conversations, and spaces. They will still be visible and accessible to already invited external users and guests” — your own users lose access, but “their membership status will not change,” and re-enabling the setting restores access to everything. Disabling the setting is therefore a control on new external collaboration only. Audit and remediate the external spaces and memberships that already exist (2.6 Step 3 inventories them), or the exposure survives the fix.
Step 3: Manage the Allowlisted Domains
- The Chat allowlist is the shared Workspace trusted-domains allowlist (also used by Drive, Sites, Classroom, Looker Studio)
- Navigate to: Account → Domains → Allowlisted domains to add/remove trusted domains
- Scope external-chat exceptions per organizational unit where possible; note that the spaces/group-DM setting above admits no such scoping
Time to Complete: ~30 minutes
Code Implementation
Code Pack: Terraform
#
# ⚠ PROVIDER STATUS (verified 2026-08-12): the hashicorp/googleworkspace provider
# was ARCHIVED by HashiCorp on 2025-06-30 and is read-only — "New releases will
# not be published." Existing binaries remain downloadable from the registry, so
# this configuration still applies, but it receives no fixes or API updates.
# Pin the version you validated, and treat a fork or a migration to API-based
# automation as a planned action rather than an emergency one.
# https://github.com/hashicorp/terraform-provider-googleworkspace
#
# The provider has never offered ANY Chat-specific resource (its full resource
# set is: chrome_policy, domain, domain_alias, gmail_send_as_alias, group,
# group_member, group_members, group_settings, org_unit, role, role_assignment,
# schema, user), which is why the blocks below build supporting structure only.
#
# External Google Chat is governed by Admin Console settings the googleworkspace
# provider does NOT expose directly:
#
# Apps > Google Workspace > Google Chat > External chat settings
# - "Allow users to send messages outside your organization" (On/Off)
# - "Only allow this for allowlisted domains"
# - "Auto-accept chat invites from familiar contacts" (disable for L2+)
# Apps > Google Workspace > Google Chat > External spaces
# - "Allow users to create & join spaces with people outside their organization"
# - "Only allow users to add people from allowlisted domains"
#
# The allowlist itself is the SHARED Workspace trusted-domains allowlist
# (Account > Domains > Allowlisted domains) -- the same allowlist used by Drive,
# Sites, Classroom, Chat, and Looker Studio.
#
# This file builds the supporting OU/group structure so external-chat access is
# an explicit, auditable exception rather than an org-wide default.
# Default OU: members are internal-only (configure "Chat externally = Off" here).
resource "googleworkspace_org_unit" "chat_internal_only" {
name = "Chat Internal Only"
description = "HTH 4.3 -- External Chat and external spaces are OFF for this OU (organization default)"
parent_org_unit_path = var.target_org_unit_path
}
# Exception OU: members may chat externally, but ONLY with allowlisted domains.
resource "googleworkspace_org_unit" "chat_external_allowlisted" {
name = "Chat External Allowlisted"
description = "HTH 4.3 -- External Chat = On with 'Only allow this for allowlisted domains' for this OU"
parent_org_unit_path = var.target_org_unit_path
}
# Group that approves which OUs/users receive the external-chat exception.
resource "googleworkspace_group" "chat_external_approvers" {
email = "chat-external-approvers@${var.primary_domain}"
name = "Chat External Approvers"
description = "HTH 4.3 -- Members approve external Chat exceptions and curate the shared allowlisted-domains list"
}
Code Pack: API Script
# Two distinct settings govern the external surface. Audit BOTH — restricting
# 1:1 external chat while leaving external spaces open leaves the larger hole.
#
# chat.external_chat_restriction
# allow_external_chat boolean
# external_chat_restriction NO_RESTRICTION | TRUSTED_DOMAINS | RESTRICTION_UNSPECIFIED
# chat.external_spaces
# enabled boolean
# domain_allowlist_mode TRUSTED_DOMAINS | ALL_DOMAINS | DOMAIN_ALLOWLIST_MODE_UNSPECIFIED
curl -s -G "https://cloudidentity.googleapis.com/v1/policies" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
--data-urlencode 'filter=setting.type.matches("chat.external_chat_restriction|chat.external_spaces")' \
| python3 -c '
import json, sys
FAIL = False
for p in json.load(sys.stdin).get("policies", []):
s = p.get("setting", {}); t = s.get("type", ""); v = s.get("value", {})
print(t, v)
# SCuBA GWS.CHAT.4.1v1: external chat off, or restricted to trusted domains.
if t.endswith("external_chat_restriction"):
if v.get("allow_external_chat") and v.get("external_chat_restriction") != "TRUSTED_DOMAINS":
print(" SCuBA GWS.CHAT.4.1v1 FAIL: external chat unrestricted"); FAIL = True
if t.endswith("external_spaces"):
if v.get("enabled") and v.get("domain_allowlist_mode") == "ALL_DOMAINS":
print(" FINDING: external spaces + group DMs open to ALL domains"); FAIL = True
sys.exit(1 if FAIL else 0)
'
Validation & Testing
- As a standard user, attempt to message a non-allowlisted external address—delivery should be blocked
- Attempt to add a non-allowlisted external user to a space—should fail
- Attempt to start a group direct message including a non-allowlisted external address—should fail
- Confirm an allowlisted-domain partner can still chat
Expected result: External chat, spaces, and group DMs work only with allowlisted domains (or are fully disabled).
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| CISA SCuBA | GWS.CHAT.4.1v1 | External chat restricted to allowlisted domains |
| NIST 800-53 | AC-20 | Use of external systems |
| SOC 2 | CC6.6 | Boundary protection / external access |
CIS note (re-checked 2026-08-12): the CIS Google Workspace Foundations Benchmark does contain Chat recommendations, and a research pass located candidate numbering for external and internal Chat file sharing. Those numbers are still deliberately not cited here, for a reason worth stating plainly: the only full benchmark PDFs reachable in that pass came from unofficial third-party mirrors rather than CIS, and the recommendation numbering was shown to shift between benchmark versions. A control ID copied from a mirror is exactly the kind of citation that looks authoritative and is unverifiable, so SCuBA remains the primary mapping. Add CIS Google Workspace numbers only from a PDF downloaded from CIS itself, and pin the benchmark version alongside them.
2.2 Restrict Google Chat File Sharing
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls | 3.3 |
| NIST 800-53 | AC-3 |
| CISA SCuBA | GWS.CHAT.2.1v1 |
Description
Limit which files users can share in Google Chat, separately for internal and external conversations. Per the CISA SCuBA baseline, external file sharing in Chat should be set to No files.
Rationale
Why This Matters:
- File sharing in Chat is a data-loss avenue that is monitored less rigorously than email or Drive
- Disabling external Chat file sharing removes an exfiltration path that DLP alone may not fully cover
- Restricting internal sharing to Images only for sensitive OUs reduces accidental document leakage
Attack Prevented: Data exfiltration via Chat attachments, malware delivery through shared files
Prerequisites
- Decision on internal sharing posture per organizational unit
- DLP for Chat configured for residual risk (Google Workspace DLP)
ClickOps Implementation
Step 1: Configure Chat File Sharing
- Navigate to: Admin Console → Apps → Google Workspace → Google Chat → Chat file sharing
- Set External filesharing to No files (SCuBA GWS.CHAT.2.1v1)
- Set Internal filesharing to Allow all files or, for sensitive OUs, Images only
- Click Save
Note: Files shared in Chat are automatically scanned for viruses before delivery, but malware and DLP scanning do not replace a file-type restriction.
Time to Complete: ~15 minutes
Code Implementation
Code Pack: Terraform
#
# ⚠ PROVIDER STATUS (verified 2026-08-12): the hashicorp/googleworkspace provider
# was ARCHIVED by HashiCorp on 2025-06-30 and is read-only — "New releases will
# not be published." Existing binaries remain downloadable from the registry, so
# this configuration still applies, but it receives no fixes or API updates.
# Pin the version you validated, and treat a fork or a migration to API-based
# automation as a planned action rather than an emergency one.
# https://github.com/hashicorp/terraform-provider-googleworkspace
#
# The provider has never offered ANY Chat-specific resource (its full resource
# set is: chrome_policy, domain, domain_alias, gmail_send_as_alias, group,
# group_member, group_members, group_settings, org_unit, role, role_assignment,
# schema, user), which is why the blocks below build supporting structure only.
#
# Chat file-sharing limits are configured in:
# Apps > Google Workspace > Google Chat > Chat file sharing
# - "External filesharing" dropdown: Allow all files | Images only | No files
# - "Internal filesharing" dropdown: Allow all files | Images only | No files
#
# The googleworkspace provider does NOT expose these dropdowns, so enforcement is
# ClickOps (SCuBA GWS.CHAT.2.1v1 requires External filesharing = "No files").
# This file creates an OU where the strictest file-sharing policy is applied,
# mirroring the Drive external-sharing pattern in control 4.1.
# OU for highly sensitive teams: configure "External filesharing = No files" and
# "Internal filesharing = Images only" here for the tightest Chat data boundary.
resource "googleworkspace_org_unit" "chat_no_file_sharing" {
name = "Chat No External File Sharing"
description = "HTH 2.2 -- External Chat file sharing set to 'No files' for this OU (SCuBA GWS.CHAT.2.1v1)"
parent_org_unit_path = var.target_org_unit_path
}
Code Pack: API Script
# chat.chat_file_sharing exposes both dropdowns as enums:
# external_file_sharing / internal_file_sharing
# ALL_FILES | IMAGES_ONLY | NO_FILES | FILE_SHARING_OPTION_UNSPECIFIED
# SCuBA GWS.CHAT.2.1v1 requires external_file_sharing = NO_FILES.
curl -s -G "https://cloudidentity.googleapis.com/v1/policies" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
--data-urlencode 'filter=setting.type.matches("chat.chat_file_sharing")' \
| python3 -c '
import json, sys
fail = False
for p in json.load(sys.stdin).get("policies", []):
v = p.get("setting", {}).get("value", {})
ou = p.get("policyQuery", {}).get("orgUnit", "(customer default)")
ext, internal = v.get("external_file_sharing"), v.get("internal_file_sharing")
print(f"orgUnit={ou} external={ext} internal={internal}")
if ext != "NO_FILES":
print(" SCuBA GWS.CHAT.2.1v1 FAIL: external Chat file sharing is not NO_FILES")
fail = True
sys.exit(1 if fail else 0)
'
Validation & Testing
- As a standard user, attempt to attach a file in an external conversation—should be blocked
- Confirm internal sharing behaves per the configured posture
- Review Chat log events for
attachment_uploadactivity
Expected result: External Chat file sharing disabled; internal sharing matches policy.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| CISA SCuBA | GWS.CHAT.2.1v1 | External Chat file sharing disabled |
| NIST 800-53 | AC-3 | Access enforcement |
| SOC 2 | CC6.6 | Boundary protection |
2.3 Enforce Google Chat History & Retention
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls | 8.2, 8.10 |
| NIST 800-53 | AU-2, AU-9, SC-7(10) |
| CISA SCuBA | GWS.CHAT.1.1v1, GWS.CHAT.1.2v1, GWS.CHAT.3.1v1 |
Description
Turn Chat history on by default, prevent users from changing their own history setting, force space history on, and use Google Vault to retain and legally hold Chat content for traceability and eDiscovery.
Rationale
Why This Matters:
- History off means direct messages are deleted after 24 hours and cannot be retained by Vault—erasing the audit trail
- Allowing users to change their history setting lets them obfuscate sensitive sharing (MITRE ATT&CK T1562.001, Impair Defenses)
- Retention and legal holds preserve Chat evidence for investigations and dispute resolution
Attack Prevented: Audit-trail tampering, evidence destruction, insider data hiding
Prerequisites
- Information-governance/retention requirements defined
- Google Vault license (Business Plus or Enterprise editions)
ClickOps Implementation
Step 1: Enforce Chat History
- Navigate to: Admin Console → Apps → Google Workspace → Google Chat → History for chats
- Select History is ON
- Uncheck Allow users to change their history setting
- Click Save
Step 2: Enforce Space History
- Navigate to: Apps → Google Workspace → Google Chat → History for spaces
- Select History is ALWAYS ON
- Click Save
Step 3: Configure Vault Retention & Holds
- In Google Vault → Retention, create a Chat retention rule by organizational unit or for all spaces; set retention for DMs, group messages, and space messages
- In Vault → Matters → Holds, place relevant accounts/OUs on a Chat hold (include spaces the user belongs to)
- Note: holds never expire and override retention rules; Chat messages are kept 30 days after deletion
The one place a Vault hold does not hold. In extra-large spaces (documented at more than 50,000 members), Chat content follows the space retention policy regardless of holds. Everywhere else a hold beats retention; here it does not. If a legal hold must cover a conversation, confirm the space is not in that category — an assumption that “the hold covers it” is exactly the assumption that fails at the moment it is tested in an investigation.
Time to Complete: ~45 minutes
Code Implementation
Code Pack: API Script
# Three SCuBA baselines are provable from two settings:
# chat.chat_history history_on_by_default (GWS.CHAT.1.1v1)
# allow_user_modification (GWS.CHAT.1.2v1 - must be false)
# chat.space_history history_state (GWS.CHAT.3.1v1)
# DEFAULT_HISTORY_ON | DEFAULT_HISTORY_OFF | HISTORY_ALWAYS_ON |
# HISTORY_ALWAYS_OFF | HISTORY_STATE_UNSPECIFIED
curl -s -G "https://cloudidentity.googleapis.com/v1/policies" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
--data-urlencode 'filter=setting.type.matches("chat.chat_history|chat.space_history")' \
| python3 -c '
import json, sys
fail = False
for p in json.load(sys.stdin).get("policies", []):
s = p.get("setting", {}); t = s.get("type", ""); v = s.get("value", {})
ou = p.get("policyQuery", {}).get("orgUnit", "(customer default)")
print(f"{t} orgUnit={ou} {v}")
if t.endswith("chat_history"):
if not v.get("history_on_by_default"):
print(" SCuBA GWS.CHAT.1.1v1 FAIL: chat history not on by default"); fail = True
if v.get("allow_user_modification"):
print(" SCuBA GWS.CHAT.1.2v1 FAIL: users can change their history setting"); fail = True
if t.endswith("space_history"):
# ALWAYS_ON is the only state users cannot turn off per space.
if v.get("history_state") != "HISTORY_ALWAYS_ON":
print(" SCuBA GWS.CHAT.3.1v1 REVIEW: space history is not HISTORY_ALWAYS_ON"); fail = True
sys.exit(1 if fail else 0)
'
Code Pack: SDK Script
from googleapiclient.discovery import build
vault = build('vault', 'v1', credentials=credentials)
# 1. Create a matter to own the hold (or reuse an existing matterId).
matter = vault.matters().create(body={
'name': 'HTH Chat Retention',
'description': 'HTH 2.3 -- preserves Google Chat content for legal/compliance hold',
}).execute()
matter_id = matter['matterId']
# 2. Place an org unit on hold for the Chat corpus, including space (room) messages.
hold = vault.matters().holds().create(matterId=matter_id, body={
'name': 'HTH Chat Hold',
'corpus': 'HANGOUTS_CHAT',
'orgUnit': {'orgUnitId': 'id:03ph8a2z1example'}, # Admin SDK org unit ID
'query': {'hangoutsChatQuery': {'includeRooms': True}},
}).execute()
print(f"Created Chat hold {hold['holdId']} on matter {matter_id}")
Code Pack: DB Query
-- Bulk message deletion is the Chat analogue of clearing shell history. A user
-- deleting far more than their own baseline is an evidence-destruction signal,
-- and is exactly what an insider does before or after exfiltration.
SELECT
email AS actor,
COUNT(*) AS messages_deleted,
MIN(TIMESTAMP_MICROS(time_usec)) AS first_deletion,
MAX(TIMESTAMP_MICROS(time_usec)) AS last_deletion,
COUNT(DISTINCT ip_address) AS distinct_source_ips
FROM `project.dataset.activity`
WHERE event_name = 'message_deleted'
AND _PARTITIONTIME >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
AND TIMESTAMP_MICROS(time_usec) >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
GROUP BY actor
HAVING messages_deleted > 25
ORDER BY messages_deleted DESC;
-- Deleting a space is a cascading delete: its messages and memberships go with
-- it. Every occurrence deserves an owner and a reason, so this is deliberately
-- unfiltered rather than thresholded.
SELECT
TIMESTAMP_MICROS(time_usec) AS event_time,
email AS actor,
event_name,
ip_address
FROM `project.dataset.activity`
WHERE event_name IN ('room_deleted', 'message_edited')
AND _PARTITIONTIME >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
AND TIMESTAMP_MICROS(time_usec) >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
ORDER BY event_time DESC;
-- Members being removed in bulk can precede a space deletion, or quietly cut
-- witnesses out of a conversation before it continues. Pair with the bulk-add
-- detection in the 3.1 pack to see both directions of membership churn.
SELECT
email AS actor,
COUNT(*) AS members_removed,
COUNT(DISTINCT DATE(TIMESTAMP_MICROS(time_usec))) AS active_days
FROM `project.dataset.activity`
WHERE event_name = 'remove_room_member'
AND _PARTITIONTIME >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
AND TIMESTAMP_MICROS(time_usec) >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
GROUP BY actor
HAVING members_removed > 25
ORDER BY members_removed DESC;
Validation & Testing
- Confirm a standard user cannot toggle history off in a conversation
- Verify the Vault retention rule and hold appear and cover the Chat corpus
- Search Chat in Vault to confirm content is discoverable
Expected result: History enforced on; users cannot change it; Chat retained per policy.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| CISA SCuBA | GWS.CHAT.1.1v1 | Chat history enabled |
| CISA SCuBA | GWS.CHAT.1.2v1 | Users cannot change history setting |
| CISA SCuBA | GWS.CHAT.3.1v1 | Space history enabled |
| NIST 800-53 | AU-9 | Protection of audit information |
| ISO 27001 | A.12.4.2 | Protection of log information |
2.4 Configure Chat Auto-Deletion Retention
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls | 3.1, 3.4 |
| NIST 800-53 | AU-11, SI-12 |
Description
Set an explicit auto-deletion period for Chat content per organizational unit, independently for 1:1 direct messages, group direct messages, and space messages. Auto-deletion bounds how long conversational data lingers in Chat — the complement to the retention floor set in 2.3.
Rationale
Why This Matters:
- Chat accumulates unbounded conversational data — credentials pasted into DMs, customer records dropped into spaces — that becomes breach blast radius the longer it is kept
- Data-minimization obligations (GDPR storage limitation, contractual retention caps) require a defensible upper bound, not just a lower one; auto-deletion is the only Chat-native mechanism that provides it
- Auto-deletion applies only to messages sent while history was ON — with history off, messages already disappear after 24 hours and this setting is irrelevant, which makes enforcing history (2.3) a prerequisite for predictable retention
Attack Prevented: Excessive data retention increasing breach impact, stale-credential harvesting from old conversations, retention-policy compliance failure
Prerequisites
- Chat history enforced ON (2.3) — auto-deletion has no effect on history-off messages
- A documented retention period per conversation type, agreed with legal/compliance
- A qualifying edition: Frontline Plus, Business Plus, Enterprise Standard/Plus, Education Standard/Plus, or Enterprise Essentials/Plus
ClickOps Implementation
Step 1: Set Auto-Deletion Periods
- Navigate to: Admin Console → Apps → Google Workspace → Google Chat → Auto-deletion
- Select the organizational unit to configure
- Set an independent retention period for each of 1:1 direct messages, group direct messages, and space messages — any value from 30 to 36,500 days
- Click Save
Only 1:1 direct messages support per-OU scoping. The group-DM and space-message periods are set for the organization, so a stricter retention period for one sensitive team is achievable for their 1:1 DMs only. Where a team genuinely needs tighter retention on spaces, the lever is a separate space with its own Vault retention rule, not this setting.
Interaction with Vault: auto-deletion does not defeat Vault. If auto-deletion fires before a Vault retention rule expires, the message is still held in Vault for the remainder of the Vault retention period, or a minimum of 30 days — whichever is longer. Configure both deliberately rather than assuming one overrides the other.
No automation surface (verified 2026-08-12): this control ships no Code Pack because none can be written honestly. Auto-deletion periods are absent from the eight Chat settings the Policy API exposes, so the setting can be neither enforced nor even read programmatically — configure and verify it in the console. Its prerequisite, history enforcement, is verifiable (3.4), so the strongest available proxy is confirming history is on and unmodifiable and reviewing the auto-deletion periods by hand on a schedule.
Time to Complete: ~30 minutes
Validation & Testing
- Confirm the configured period appears for each of the three conversation types on the target OU
- Post a test message in a history-on space, and confirm it is no longer visible in Chat after the retention period elapses
- Confirm the same message remains discoverable in Vault if it falls under an active retention rule or hold
Expected result: Chat content is deleted on a defined schedule per conversation type, while Vault-governed content stays preserved for its own retention window.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| NIST 800-53 | AU-11 | Audit record retention |
| NIST 800-53 | SI-12 | Information management and retention |
| SOC 2 | CC6.5 | Data disposal |
| ISO 27001 | A.8.10 | Information deletion |
2.5 Apply DLP to Google Chat Messages and Attachments
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls | 3.13 |
| NIST 800-53 | AC-4, SC-7(10), SI-4 |
Description
Create Chat-scoped data protection rules under Security → Access and data control → Data protection, which inspect message text and attachments against content detectors and then block, warn, or audit. This is the only Chat control that inspects content even when Chat history is off.
Rationale
Why This Matters:
- DLP for Chat scans messages even when history is turned off, covering 1:1 DMs, group DMs, spaces, and attachments including images — this is the single control that survives the history-off blind spot that simultaneously breaks content reporting (3.1) and Vault retention (2.3)
- File-type restrictions (2.2) stop categories of files but say nothing about what is inside them — a permitted image can carry a screenshot of a customer database
- Warn-mode rules convert an invisible policy into an in-the-moment user prompt, which reduces accidental disclosure without blocking legitimate work
Attack Prevented: Sensitive-data exfiltration over Chat, inadvertent disclosure of regulated data in messages or attachments, evasion of message-level monitoring by disabling history
Prerequisites
- A qualifying edition: Frontline Standard/Plus, Enterprise Standard/Plus, or Education Fundamentals/Standard/Plus
- Defined sensitive-data categories and the detectors that match them
- Platform DLP context (Google Workspace DLP)
ClickOps Implementation
Step 1: Create a Chat-Scoped Data Protection Rule
- Navigate to: Admin Console → Security → Access and data control → Data protection → Manage Rules → Add rule
- Name the rule and scope it to the target organizational units or groups
- Under the applications/triggers step, select Google Chat, and include message text and attachments in scope
- Define the conditions using content detectors that match your sensitive-data categories
- Choose an action: Block message, Warn users, or Audit only
- Set the alerting level and click Create
Rule conflicts: when multiple rules match the same message, the most restrictive action wins — block takes precedence over warn, and warn over audit only. Start in Audit only to measure false positives, then promote to warn and block.
No automation surface (verified 2026-08-12): data protection rules are not among the Chat settings the Policy API exposes, so this control ships no Code Pack — rules are authored and reviewed in the console. Two facts make manual review tractable: the Chat triggers are identified as
google.workspace.chat.message.v1.sendandgoogle.workspace.chat.attachment.v1.upload, and rule hits are visible in the security investigation tooling even though the rule definitions are not readable by API. Review the rule set on the same cadence as 3.4’s drift check, since a deleted DLP rule leaves no trace in a settings baseline.
Time to Complete: ~60 minutes
Validation & Testing
- From a test account in scope, send a message containing synthetic data matching a rule condition — confirm the configured block or warn behavior fires
- Repeat the test in a conversation with history turned off — the rule must still fire, which is the point of this control
- Attach an image containing matching content and confirm the attachment is inspected
- Confirm rule hits appear in the security investigation tooling
Expected result: Sensitive content is blocked, warned on, or audited in Chat messages and attachments across every conversation type, independent of the history setting.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| NIST 800-53 | AC-4 | Information flow enforcement |
| NIST 800-53 | SI-4 | System monitoring |
| SOC 2 | CC6.7 | Restricted data transmission |
| ISO 27001 | A.8.12 | Data leakage prevention |
2.6 Set the Default Space Access to Restricted
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls | 3.3 |
| NIST 800-53 | AC-3, AC-6 |
Description
Set the organization’s default space access to Restricted, so that a newly created Chat space is joinable only by the people and groups explicitly added to it — rather than defaulting to a target audience that makes the space discoverable and joinable org-wide.
Rationale
Why This Matters:
- The Primary Target Audience default makes every newly created space discoverable and joinable by anyone in that audience, and the shipped default audience is all users in your domain — so a space created for a sensitive project is domain-discoverable by default, and private only if its creator remembers to change it
- Space membership grants access to the space’s full message history and shared files, so an over-broad default silently widens access to everything ever posted there
- Secure defaults beat user diligence: the creator of a legal, HR, or incident-response space is rarely thinking about discoverability at the moment of creation
Attack Prevented: Unauthorized internal access to sensitive discussions, insider browsing of restricted-project spaces, over-broad exposure of files shared in spaces
Prerequisites
- Decision on whether any target audience should be offered to space creators at all
- Target audiences defined if used (a maximum of five apply to Chat)
ClickOps Implementation
Step 1: Set the Space Access Default
- Navigate to: Admin Console → Apps → Google Workspace → Google Chat → Sharing settings → Space access default
- Select Restricted — only people and groups explicitly added can join the space
- Click Save
Step 2: Review Target Audiences (if used)
- Navigate to: Admin Console → Apps → Google Workspace → Google Chat → Sharing settings → Target audiences
- Confirm only intentional audiences are listed; up to five apply to Chat, and the audience in the first position becomes the recommended default shown to space creators
- Order the list so the narrowest appropriate audience sits first — never leave a broad organization-wide audience in the first position
- Click Save
Note: this sets the default, not a prohibition. Space creators can still widen access to a target audience; pair the restricted default with the external-chat restrictions in 2.1 to bound how far a space can be widened.
Step 3: Inventory the Spaces That Already Exist
- A default only governs spaces created after it is set — it says nothing about the spaces already in the tenant, which is where the accumulated exposure lives
- Grant the Manage Chat and Spaces conversation admin privilege to the account that will run the inventory
- Use the Chat API’s admin space search (
spaces.searchwithuseAdminAccess) to enumerate every named space, then review three populations: spaces with no recent activity, spaces with no remaining manager, and spaces whose membership includes external users - Re-run on a schedule — space sprawl is continuous, so a one-off inventory ages out immediately
Time to Complete: ~20 minutes (plus inventory review)
Code Implementation
Code Pack: CLI Script
# Org-wide space inventory via admin access. Both `customer` and `spaceType`
# are REQUIRED by the API, and each currently accepts exactly one value:
# customer = "customers/my_customer"
# spaceType = "SPACE"
gws chat spaces search \
--params '{"useAdminAccess":true,"query":"customer = \"customers/my_customer\" AND spaceType = \"SPACE\""}' \
--page-all --format json
# Dormant spaces still carry their full message history and shared files, so an
# abandoned space is standing exposure. Find spaces with no activity since a
# cutoff, then review before deleting.
CUTOFF="2026-01-01T00:00:00+00:00"
gws chat spaces search \
--params "{\"useAdminAccess\":true,\"query\":\"customer = \\\"customers/my_customer\\\" AND spaceType = \\\"SPACE\\\" AND lastActiveTime < \\\"${CUTOFF}\\\"\"}" \
--page-all --format json
# List the members of a specific space to identify external members and
# confirm the space still has an owner/manager.
gws chat spaces members list \
--params '{"parent":"spaces/SPACE_ID","useAdminAccess":true}' \
--format json
Validation & Testing
- As a standard user, create a new space and confirm the access setting defaults to restricted rather than to a target audience
- From a second account not added to that space, confirm the space is not discoverable in Chat search
- If target audiences are used, confirm the first-position audience is the intended narrow one
- Run the admin space search and confirm it returns spaces — an empty result usually means the caller is missing the Manage Chat and Spaces conversation privilege rather than that the tenant has no spaces
- Confirm the Policy API reports
chat.space_access_defaultaccess_typeasRESTRICTED(3.4)
Expected result: New spaces are private by default and joinable only by explicitly added members, and the existing space estate has been inventoried rather than assumed.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| NIST 800-53 | AC-3 | Access enforcement |
| NIST 800-53 | AC-6 | Least privilege |
| SOC 2 | CC6.1 | Logical access security |
| ISO 27001 | A.5.15 | Access control |
2.7 Govern Third-Party Chat Archiving
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls | 3.3 |
| NIST 800-53 | AC-4, AU-9, SI-4 |
Description
Establish whether Chat’s third-party archiving is enabled and, if it is, that its destination is an address your organization actually approved. The setting delivers Chat content to an external email address on a recurring schedule (documented as every 1–24 hours), and it is exposed for reading through the Policy API as chat.third_party_archiving.
Rationale
Why This Matters:
- This is a platform-sanctioned channel that continuously ships Chat content out of the tenant — configured deliberately it is a compliance archive, configured carelessly it is a permanent exfiltration path that no DLP rule inspects, because Google itself is doing the sending
- The destination is a plain email address, so a single mistyped or maliciously altered value redirects the organization’s conversations without breaking anything a user would notice
- Archiving destinations outlive the vendor relationships that justified them: an address belonging to a decommissioned archiving provider keeps receiving content until someone thinks to look
Attack Prevented: Sanctioned-channel data exfiltration, persistence of Chat content delivery to a decommissioned or attacker-controlled destination, silent redirection of an organization’s conversation archive
Prerequisites
- A documented decision on whether third-party archiving is used at all, and the approved destination address if it is
- Policy API access for verification (3.4)
ClickOps Implementation
Step 1: Determine the Current State
- Navigate to: Admin Console → Apps → Google Workspace → Google Chat
- Locate the third-party archiving configuration and record whether it is enabled, its destination email address, and its archival frequency
Step 2: Decide and Document
- If archiving is not required, confirm it is disabled
- If it is required, record the approved destination address and frequency in your data-flow documentation, and treat that address as a reviewed egress destination rather than an implementation detail
- Re-confirm the destination whenever the archiving vendor relationship changes
Code Implementation
Code Pack: API Script
# chat.third_party_archiving fields:
# enabled boolean
# destination_email_address string <- the address Chat content is sent to
# archival_frequency Duration (documented range 1-24 hours)
# custom headers comma-separated string
#
# Set ARCHIVE_ALLOWED to the address your organization has actually approved.
ARCHIVE_ALLOWED="${ARCHIVE_ALLOWED:-}"
curl -s -G "https://cloudidentity.googleapis.com/v1/policies" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
--data-urlencode 'filter=setting.type.matches("chat.third_party_archiving")' \
| ARCHIVE_ALLOWED="${ARCHIVE_ALLOWED}" python3 -c '
import json, os, sys
allowed = os.environ.get("ARCHIVE_ALLOWED", "").strip().lower()
fail = False
for p in json.load(sys.stdin).get("policies", []):
v = p.get("setting", {}).get("value", {})
ou = p.get("policyQuery", {}).get("orgUnit", "(customer default)")
if not v.get("enabled"):
print(f"orgUnit={ou} third-party archiving DISABLED")
continue
dest = (v.get("destination_email_address") or "").lower()
print(f"orgUnit={ou} ENABLED -> {dest} every {v.get(\"archival_frequency\")}")
if not allowed:
print(" REVIEW: archiving is on and no approved destination was supplied")
fail = True
elif dest != allowed:
print(f" FINDING: Chat content is delivered to an UNAPPROVED address ({dest})")
fail = True
sys.exit(1 if fail else 0)
'
Validation & Testing
- Read
chat.third_party_archivingthrough the Policy API and confirmenabledmatches your documented decision - If enabled, confirm
destination_email_addressexactly equals the approved address — the check is string equality against a recorded value, not a judgement call - Confirm the configured
archival_frequencymatches what your retention documentation claims
Expected result: Third-party archiving is either off, or on with a destination that matches an approved, documented address.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| NIST 800-53 | AC-4 | Information flow enforcement |
| NIST 800-53 | AU-9 | Protection of audit information |
| SOC 2 | CC6.7 | Restricted data transmission |
| ISO 27001 | A.8.12 | Data leakage prevention |
3. Monitoring & Detection
3.1 Enable Google Chat Audit Logging & Content Reporting
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 8.2, 8.5 |
| NIST 800-53 | AU-2, AU-6, IR-6 |
| CISA SCuBA | GWS.CHAT.5.1v1, GWS.CHAT.5.2v1 |
Description
Monitor Google Chat through the Chat log events report (and Reports API / BigQuery export), and — on editions that support it — enable content reporting so users can flag malicious or inappropriate messages to admins across every conversation type the feature covers.
Rationale
Why This Matters:
- Chat is a phishing and malware-delivery channel; content reporting turns every user into a detection sensor (NIST IR-6)
- Chat audit events (
message_posted,attachment_upload,room_created,add_room_member) reveal exfiltration and rogue-space activity - Reporting requires Chat history to be enabled (2.3)
Attack Prevented: Undetected Chat phishing/malware, unmonitored data exfiltration, delayed incident response
Prerequisites
- Chat history enabled (2.3)
- Audit & Investigation admin privilege; BigQuery export for long-term retention (Google Workspace Audit Logging)
- Content reporting is not available on every edition. It requires Frontline Plus, Enterprise Plus, or Education Standard/Plus. Chat log events and the BigQuery export in Step 1 are independent of this and remain available more broadly — organizations without a qualifying edition should implement Step 1 and rely on 3.2 Chat content protection and 2.5 DLP for Chat for detection coverage
- A designated owner for the report queue (3.3)
Documented blind spots: users cannot report messages in (a) conversations with history turned off, (b) 1:1 direct messages with external users, or (c) spaces owned by an external organization. These gaps are by design and are not closed by selecting every conversation-type checkbox — cover them with DLP for Chat, which scans even when history is off, and by enforcing history on (2.3).
ClickOps Implementation
Step 1: Review Chat Log Events
- Navigate to: Admin Console → Reporting → Audit and investigation → Chat log events
- Filter by Event (e.g.,
attachment_upload,room_created) and date range - For advanced triage (Enterprise Standard/Plus): Security → Security center → Investigation tool, data source Chat log events
Step 2: Enable Content Reporting
- Navigate to: Apps → Google Workspace → Google Chat → Content reporting
- Enable Allow users to report content in Chat
- Select all conversation type checkboxes (1:1, group, spaces) — SCuBA GWS.CHAT.5.1v1
- Select all reporting categories — SCuBA GWS.CHAT.5.2v1
- Click Save
Time to Complete: ~30 minutes
Key Chat Events to Monitor
Event names below are transcribed from the Admin SDK Reports API Chat activity-events appendix. Two parameters are worth knowing because they carry the internal/external distinction the event name alone does not: conversation_ownership (INTERNALLY_OWNED / EXTERNALLY_OWNED) and conversation_type (which includes GROUP_DIRECT_MESSAGE).
| Event | Detection Use Case |
|---|---|
attachment_upload |
Data exfiltration into Chat |
attachment_download |
Data exfiltration out of Chat — a compromised account harvesting an existing space downloads without ever uploading |
message_posted |
Phishing / malicious link distribution |
message_deleted |
Evidence destruction after exfiltration or misconduct |
message_edited |
Retroactive alteration of preserved content |
room_created |
Rogue or external space creation |
room_deleted |
Cascading destruction of a space, its messages, and its memberships |
add_room_member |
Users added to spaces in bulk |
remove_room_member |
Witnesses removed from a conversation before it continues |
direct_message_started |
External DM initiation (check conversation_ownership) |
app_added / app_removed |
Chat app installation outside the approved allowlist workflow (1.1) |
app_invoked |
Chat app driven programmatically far beyond its business purpose |
message_reported / message_report_resolved |
Content-reporting queue throughput — raised versus resolved is how you prove the queue in 3.3 is actually worked |
Code Implementation
Code Pack: API Script
# Attachment uploads — data moving INTO Chat.
curl -s -G "${BASE}" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
--data-urlencode "eventName=attachment_upload" \
--data-urlencode "startTime=$(date -u -v-7d '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || date -u -d '7 days ago' '+%Y-%m-%dT%H:%M:%SZ')"
# Attachment downloads — data moving OUT. A compromised account harvesting an
# existing space generates downloads without ever uploading anything.
curl -s -G "${BASE}" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
--data-urlencode "eventName=attachment_download"
# Space lifecycle and membership growth (rogue or external spaces).
for EV in room_created add_room_member; do
curl -s -G "${BASE}" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
--data-urlencode "eventName=${EV}"
done
# Content-reporting queue health. `message_reported` and `message_report_resolved`
# both carry a `report_id` parameter, so raised-versus-resolved is measurable —
# which is what proves the queue in control 3.3 has a working owner.
curl -s -G "${BASE}" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
--data-urlencode "eventName=message_reported" \
| python3 -c '
import json, sys
items = json.load(sys.stdin).get("items", [])
print(f"messages reported: {len(items)}")
for a in items:
actor = a.get("actor", {}).get("email", "unknown")
print(f" {a[\"id\"][\"time\"]} {actor}")
'
curl -s -G "${BASE}" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
--data-urlencode "eventName=message_report_resolved"
# Deletion and editing of content that history was meant to preserve
# (MITRE ATT&CK T1562.001, Impair Defenses).
for EV in message_deleted message_edited room_deleted; do
curl -s -G "${BASE}" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
--data-urlencode "eventName=${EV}"
done
Code Pack: CLI Script
# All Chat activity for the customer. `--params` carries query parameters as
# JSON; `--page-all` auto-paginates and emits NDJSON.
gws admin-reports activities list \
--params '{"userKey":"all","applicationName":"chat"}' \
--page-all --format json
# Attachment uploads only — the Chat data-exfiltration signal.
gws admin-reports activities list \
--params '{"userKey":"all","applicationName":"chat","eventName":"attachment_upload"}' \
--page-all --format json
# Rogue/external space activity: creation and membership growth.
gws admin-reports activities list \
--params '{"userKey":"all","applicationName":"chat","eventName":"room_created"}' \
--format json
gws admin-reports activities list \
--params '{"userKey":"all","applicationName":"chat","eventName":"add_room_member"}' \
--format json
# Bound the window for a scheduled review. startTime/endTime are RFC 3339.
# --dry-run validates the call locally without hitting the API.
WINDOW_START="$(date -u -v-7d '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || date -u -d '7 days ago' '+%Y-%m-%dT%H:%M:%SZ')"
gws admin-reports activities list \
--params "{\"userKey\":\"all\",\"applicationName\":\"chat\",\"startTime\":\"${WINDOW_START}\"}" \
--page-all --page-limit 50 --format json \
> "chat-audit-$(date -u '+%Y-%m-%d').ndjson"
Code Pack: SDK Script
# Pull Google Chat audit events from the Admin SDK Reports API
# (applicationName='chat'). Verified event names include: message_posted,
# attachment_upload, room_created, add_room_member, remove_room_member.
from googleapiclient.discovery import build
reports = build('admin', 'reports_v1', credentials=credentials)
# All Chat attachment uploads in the last 7 days (potential data exfiltration).
resp = reports.activities().list(
userKey='all',
applicationName='chat',
eventName='attachment_upload',
startTime='2026-05-20T00:00:00Z',
).execute()
for activity in resp.get('items', []):
actor = activity['actor'].get('email', 'unknown')
for event in activity.get('events', []):
print(f"{activity['id']['time']} {actor} {event['name']}")
Code Pack: DB Query
-- Surface users uploading an unusually high volume of Chat attachments
-- (potential data exfiltration through Google Chat).
SELECT
email,
COUNT(*) AS attachments_uploaded
FROM `project.dataset.activity`
WHERE event_name = 'attachment_upload'
AND _PARTITIONTIME >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
AND TIMESTAMP_MICROS(time_usec) >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
GROUP BY email
HAVING attachments_uploaded > 50
ORDER BY attachments_uploaded DESC;
-- Flag accounts posting Chat messages at an abnormal rate (phishing or
-- malicious-link distribution). A high distinct-source-IP count alongside
-- high volume suggests a compromised account rather than a chatty user.
SELECT
email,
COUNT(*) AS messages_posted,
COUNT(DISTINCT ip_address) AS distinct_source_ips
FROM `project.dataset.activity`
WHERE event_name = 'message_posted'
AND _PARTITIONTIME >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 DAY)
AND TIMESTAMP_MICROS(time_usec) >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 DAY)
GROUP BY email
HAVING messages_posted > 200
ORDER BY messages_posted DESC;
-- Track Google Chat space (room) creation to spot rogue or external spaces.
SELECT
email,
COUNT(*) AS spaces_created
FROM `project.dataset.activity`
WHERE event_name = 'room_created'
AND _PARTITIONTIME >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
AND TIMESTAMP_MICROS(time_usec) >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
GROUP BY email
ORDER BY spaces_created DESC;
-- The upload query above catches data going INTO Chat; this catches it coming
-- back OUT. A compromised account harvesting an existing space's attachment
-- history generates downloads without ever uploading anything, so a
-- upload-only detection misses the entire read side of exfiltration.
SELECT
email AS actor,
COUNT(*) AS attachments_downloaded,
COUNT(DISTINCT ip_address) AS distinct_source_ips,
MIN(TIMESTAMP_MICROS(time_usec)) AS window_start,
MAX(TIMESTAMP_MICROS(time_usec)) AS window_end
FROM `project.dataset.activity`
WHERE event_name = 'attachment_download'
AND _PARTITIONTIME >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
AND TIMESTAMP_MICROS(time_usec) >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
GROUP BY actor
HAVING attachments_downloaded > 50
ORDER BY attachments_downloaded DESC;
-- Content reporting (this control) creates a queue; control 3.3 assigns it an
-- owner. This measures whether the queue is actually being worked: reports
-- raised versus reports resolved, and the backlog between them. A queue with
-- reports and no resolutions is a detection control producing no detections.
WITH reports AS (
SELECT
DATE(TIMESTAMP_MICROS(time_usec)) AS day,
COUNTIF(event_name = 'message_reported') AS reported,
COUNTIF(event_name = 'message_report_resolved') AS resolved
FROM `project.dataset.activity`
WHERE event_name IN ('message_reported', 'message_report_resolved')
AND _PARTITIONTIME >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
AND TIMESTAMP_MICROS(time_usec) >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
GROUP BY day
)
SELECT
day,
reported,
resolved,
reported - resolved AS unresolved_delta,
SUM(reported - resolved) OVER (ORDER BY day) AS running_backlog
FROM reports
ORDER BY day DESC;
-- Surface actors adding space members in bulk (rogue-space population or
-- staging for exfiltration). Triage hits in Admin Console > Chat log events,
-- where the add_room_member entry lists the added target users, to confirm
-- whether the added members are external to your domain.
SELECT
email,
COUNT(*) AS members_added
FROM `project.dataset.activity`
WHERE event_name = 'add_room_member'
AND _PARTITIONTIME >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
AND TIMESTAMP_MICROS(time_usec) >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
GROUP BY email
HAVING members_added > 25
ORDER BY members_added DESC;
Validation & Testing
- As a user, confirm the Report option appears on messages in each enabled conversation type (expect it to be absent in history-off conversations, external 1:1 DMs, and externally owned spaces — those are documented gaps, not misconfiguration)
- Submit a test report and confirm it surfaces in the Moderation Tool for the assigned moderator (3.3)
- Run the Reports API / GAM query and confirm Chat events return
Expected result: Chat events are auditable; users can report content in every conversation type the feature covers, and each report reaches a named owner.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| CISA SCuBA | GWS.CHAT.5.1v1 | Content reporting enabled for all conversation types |
| CISA SCuBA | GWS.CHAT.5.2v1 | All reporting categories selected |
| NIST 800-53 | IR-6 | Incident reporting |
| SOC 2 | CC7.2 | System monitoring |
3.2 Understand Chat Content Protection Coverage Limits
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 10.1, 13.1 |
| NIST 800-53 | SI-3, SI-8 |
Description
Confirm Chat content protection is on for external chat, and document what it does not cover: Google scans messages exchanged with external users for spam, phishing, and malware, and virus-scans files for all users — but internal-only conversations are not scanned for spam or phishing. Treat that gap as a deliberate detection boundary to be covered by other controls.
Rationale
Why This Matters:
- Content protection is on by default for external chat, which makes it easy to assume Chat is uniformly scanned — it is not, and an internal-only phishing or lateral-movement message passes through unscanned
- Compromised internal accounts are exactly the case where internal-only messaging is used to move laterally, so the unscanned surface is the one an attacker who already has a foothold will use
- File virus-scanning does apply to all users, so the residual internal gap is specifically spam/phishing content — that is what DLP for Chat and content reporting exist to cover
Attack Prevented: Internal phishing and lateral movement via Chat, misplaced reliance on automatic scanning, malware delivery through Chat attachments
Prerequisites
- External chat posture decided (2.1)
- Compensating coverage in place for internal conversations: 2.5 and 3.1
ClickOps Implementation
Step 1: Confirm Chat Content Protection
- Navigate to: Admin Console → Apps → Google Workspace → Google Chat → Security & moderation → Chat content protection
- Confirm protection is enabled for chats with external users (on by default)
- Click Save if any change was made
Step 2: Close the Internal Gap
- Record in your detection coverage documentation that internal-only Chat conversations are not scanned for spam or phishing
- Ensure a DLP for Chat rule (2.5) covers internal conversations for the sensitive-data categories that matter
- Ensure content reporting (3.1) is enabled for internal 1:1, group, and space conversations so users can flag what automation does not catch
No automation surface (verified 2026-08-12): Chat content protection is not among the eight Chat settings the Policy API exposes, so its state cannot be read programmatically and this control ships no Code Pack. That makes the documentation step above the actual deliverable — the control’s value is a written, reviewed coverage boundary, not a toggle.
Time to Complete: ~20 minutes
Validation & Testing
- Confirm the content protection setting is enabled for external chat
- Send a benign test file internally and confirm it is virus-scanned before delivery
- Confirm your detection coverage matrix explicitly names internal-only spam/phishing as covered by DLP and user reporting rather than by Google’s scanning
Expected result: External Chat is scanned by Google; the internal gap is documented and covered by DLP plus user reporting rather than assumed away.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| NIST 800-53 | SI-3 | Malicious code protection |
| NIST 800-53 | SI-8 | Spam protection |
| SOC 2 | CC7.1 | Detection of anomalies |
| ISO 27001 | A.8.7 | Protection against malware |
3.3 Delegate a Scoped Chat Moderator Role
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls | 5.4, 17.4 |
| NIST 800-53 | AC-6(5), IR-4, IR-6 |
Description
Create a custom admin role carrying the Moderate Chat content report privilege and assign it to the people who will triage reported Chat content in the Workspace Moderation Tool — so reports have a named owner without granting super admin.
Rationale
Why This Matters:
- Enabling content reporting (3.1) creates a queue; a queue nobody owns is a detection control that produces no detections, which is the same unmonitored-queue failure that quarantines exhibit in the Gmail guide
- Without a scoped role, the only way to triage reports is to hand out super admin, which trades a moderation need for full tenant control — a permanent privilege escalation to solve a temporary workflow problem
- A named moderator establishes a measurable response time for user-reported phishing, which is what turns reporting into an incident-response input (NIST IR-4)
Attack Prevented: Unactioned phishing reports, privilege over-provisioning to enable moderation, delayed incident response to user-reported Chat threats
Prerequisites
- Content reporting enabled and on a qualifying edition (3.1)
- Named individuals or a security/IT group to own report triage
- A documented triage SLA and escalation path
ClickOps Implementation
Step 1: Create the Custom Role
- Navigate to: Admin Console → Account → Admin roles → Create new role
- Name it (for example,
Chat Content Moderator) and add a description naming the triage owner and SLA - In the privileges list, select Moderate Chat content report — and nothing else
- Click Create role
Step 2: Assign the Role
- Open the new role → Admins → Assign users
- Assign only the individuals responsible for triage
- Click Assign role
Step 3: Operate the Queue
- Direct assigned moderators to the Workspace Moderation Tool to review reported Chat content
- Review the queue on the cadence set by your SLA and record dispositions
- Re-review role assignments whenever team membership changes
Time to Complete: ~30 minutes
Code Implementation
Unlike most controls in this guide, this one is fully automatable in both directions: the Admin SDK Directory API can create the custom role, assign it, and audit it. Discover the privilege identifier from your own tenant rather than hardcoding one — the pack’s first step prints the Chat-related privileges with their serviceId.
Code Pack: API Script
# STEP 1 — discover the exact privilege, never guess it. Privilege identifiers
# and their serviceId are tenant-visible facts; print the Chat-related ones and
# copy the pair you need into step 2.
curl -s -G "${BASE}/privileges" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
| python3 -c '
import json, sys
def walk(items, depth=0):
for p in items:
name = p.get("privilegeName", "")
if "CHAT" in name.upper() or "MODERAT" in name.upper():
print(f"{name} serviceId={p.get(\"serviceId\")}")
walk(p.get("childPrivileges", []), depth + 1)
walk(json.load(sys.stdin).get("items", []))
'
# STEP 2 — create the custom role carrying ONLY the moderation privilege.
# Substitute PRIVILEGE_NAME and SERVICE_ID with the pair printed by step 1.
curl -s -X POST "${BASE}/roles" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"roleName": "Chat Content Moderator",
"roleDescription": "HTH 3.3 -- triages user-reported Google Chat content in the Moderation Tool. Single privilege by design: moderation must not require super admin.",
"rolePrivileges": [
{ "privilegeName": "PRIVILEGE_NAME", "serviceId": "SERVICE_ID" }
]
}'
# STEP 3 — ongoing: prove the role stayed narrow. A role that accumulates extra
# privileges, or assignees who left the triage rota, is the privilege-creep this
# control exists to prevent.
curl -s -G "${BASE}/roles" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
| python3 -c '
import json, sys
for r in json.load(sys.stdin).get("items", []):
if "moderator" in r.get("roleName", "").lower():
privs = r.get("rolePrivileges", [])
print(f"{r[\"roleName\"]} (roleId={r[\"roleId\"]}) privileges={len(privs)}")
for p in privs:
print(" ", p.get("privilegeName"))
if len(privs) > 1:
print(" FINDING: role carries more than the single moderation privilege")
'
# Who currently holds it (pass the roleId from above).
curl -s -G "${BASE}/roleassignments" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
--data-urlencode "roleId=ROLE_ID"
Validation & Testing
- Confirm the custom role lists Moderate Chat content report and no additional privileges
- As an assigned moderator, open the Moderation Tool and confirm reported content is visible and actionable
- As a non-assigned user, confirm the Moderation Tool is inaccessible
- Submit a test report from 3.1 and confirm it reaches the queue and is dispositioned within the SLA
- Measure the queue rather than trusting it: compare
message_reportedagainstmessage_report_resolvedcounts over the review window (3.1 Code Pack) — a rising unresolved backlog means the role exists but the SLA does not
Expected result: Reported Chat content is triaged by a named owner holding one narrowly scoped privilege, not by a super admin, and the resolution rate is measurable rather than assumed.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| NIST 800-53 | AC-6(5) | Privileged accounts |
| NIST 800-53 | IR-4 | Incident handling |
| SOC 2 | CC7.3 | Evaluation of security events |
| ISO 27001 | A.5.25 | Assessment and decision on information security events |
3.4 Continuously Verify Chat Configuration with the Policy API
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls | 4.2, 8.5 |
| NIST 800-53 | CM-2, CM-3, CM-6, SI-4 |
Description
Read every Chat setting the Cloud Identity Policy API exposes and compare it against this guide’s baseline on a schedule, so configuration drift is detected rather than discovered during an incident. The API covers eight Chat settings — chat.chat_apps_access, chat.external_chat_restriction, chat.external_spaces, chat.chat_file_sharing, chat.chat_history, chat.space_history, chat.space_access_default, and chat.third_party_archiving — and is read-only for all of them.
Rationale
Why This Matters:
- Every other Chat control in this guide is configured by hand in a console, which means every one of them can be changed by hand in a console — usually for a good short-term reason (“open external chat for the migration”) that nobody remembers to close
- Reading the settings in code turns the SCuBA baselines from a point-in-time audit answer into a continuously testable assertion:
history_on_by_default,allow_user_modification,external_file_sharing, andexternal_chat_restrictionmap directly onto GWS.CHAT.1.1v1, 1.2v1, 2.1v1, and 4.1v1 - Drift detection is the only control here that catches the removal of another control, which is exactly what an attacker with admin access does before acting
Attack Prevented: Silent weakening of Chat security settings by an admin or a compromised admin account, undetected configuration drift between audits, unnoticed expiry of a temporary exception
Prerequisites
- Super administrator — the Policy API is restricted to super admins
- A service account with domain-wide delegation, granted
https://www.googleapis.com/auth/cloud-identity.policies.readonly - A recorded baseline of intended values per organizational unit
Read-only, by design. Every Chat setting reports
Mutate supported: No. This control proves and monitors configuration; it cannot set it. Treat a finding as a ticket against the ClickOps control that owns the setting, not as something the script can remediate.
ClickOps Implementation
Step 1: Authorize the Caller
- Create (or reuse) a service account and enable domain-wide delegation for it
- Navigate to: Admin Console → Security → Access and data control → API controls → Domain-wide delegation → Add new
- Add the service account’s client ID with the scope
https://www.googleapis.com/auth/cloud-identity.policies.readonly - Have it impersonate a super administrator — lesser admin roles cannot read policies
Step 2: Schedule the Comparison
- Run the posture check against your recorded baseline on a schedule (daily is proportionate; the settings change rarely and drift matters immediately)
- Route findings to the same queue that owns the underlying control, and treat an unexplained change as an access-review trigger for whoever made it
Time to Complete: ~45 minutes
Code Implementation
Code Pack: SDK Script
from googleapiclient.discovery import build
# Every Chat setting type the Policy API exposes, mapped to the guide control
# that owns it and the value this guide recommends.
BASELINE = {
"chat.chat_apps_access": {
"control": "1.1",
"expect": {"enable_apps": False, "enable_webhooks": False},
},
"chat.external_chat_restriction": {
"control": "2.1",
"expect": {"external_chat_restriction": "TRUSTED_DOMAINS"}, # or allow_external_chat False
},
"chat.external_spaces": {
"control": "2.1",
"expect": {"domain_allowlist_mode": "TRUSTED_DOMAINS"},
},
"chat.chat_file_sharing": {
"control": "2.2",
"expect": {"external_file_sharing": "NO_FILES"}, # SCuBA GWS.CHAT.2.1v1
},
"chat.chat_history": {
"control": "2.3",
"expect": {"history_on_by_default": True, "allow_user_modification": False},
},
"chat.space_history": {
"control": "2.3",
"expect": {"history_state": "HISTORY_ALWAYS_ON"},
},
"chat.space_access_default": {
"control": "2.6",
"expect": {"access_type": "RESTRICTED"},
},
"chat.third_party_archiving": {
"control": "2.7",
"expect": {"enabled": False}, # any archiving destination must be reviewed
},
}
service = build("cloudidentity", "v1", credentials=credentials)
# One call returns every Chat policy; filter syntax is `setting.type.matches(...)`.
policies = service.policies().list(
filter='setting.type.matches("chat.*")'
).execute().get("policies", [])
findings = []
for policy in policies:
setting = policy.get("setting", {})
# Setting types come back namespaced, e.g. "settings/chat.chat_history".
stype = setting.get("type", "").split("/")[-1]
value = setting.get("value", {})
spec = BASELINE.get(stype)
if not spec:
continue
target = policy.get("policyQuery", {}).get("orgUnit") or "(customer default)"
for field, want in spec["expect"].items():
got = value.get(field)
if got != want:
findings.append(
f"[{spec['control']}] {stype}.{field} = {got!r} (expected {want!r}) @ {target}"
)
print(f"Chat policies read: {len(policies)}")
for f in findings:
print("FINDING:", f)
print("PASS" if not findings else f"{len(findings)} finding(s)")
# Discovery aid: list every Chat policy the tenant actually returns, including
# settings this guide does not yet model. Run this after a Workspace release to
# spot new Chat settings before they drift.
for policy in policies:
setting = policy.get("setting", {})
print(setting.get("type"), "->", setting.get("value"))
Validation & Testing
- Run the posture check against a known-good tenant and confirm it reports no findings
- Deliberately change one non-production setting (for example, set internal file sharing to a different value on a test OU) and confirm the next run flags exactly that setting and organizational unit
- Confirm a non-super-admin caller is rejected — if a lesser role appears to work, verify what it actually returned rather than assuming coverage
- Enumerate all returned policies after a Workspace release and confirm no new
chat.*setting has appeared that your baseline does not model
Expected result: Every Chat setting the Policy API exposes is compared to a recorded baseline on a schedule, and any deviation produces a finding naming the setting, the value, and the organizational unit.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| NIST 800-53 | CM-2 | Baseline configuration |
| NIST 800-53 | CM-3 | Configuration change control |
| NIST 800-53 | SI-4 | System monitoring |
| SOC 2 | CC7.1 | Detection of configuration changes |
| ISO 27001 | A.8.9 | Configuration management |
4. Compliance Quick Reference
CISA SCuBA Google Chat Baseline Mapping
| SCuBA Baseline | Control | This Guide |
|---|---|---|
| GWS.CHAT.1.1v1 | Chat history enabled | 2.3 |
| GWS.CHAT.1.2v1 | Users cannot change history setting | 2.3 |
| GWS.CHAT.2.1v1 | External Chat file sharing disabled | 2.2 |
| GWS.CHAT.3.1v1 | Space history enabled | 2.3 |
| GWS.CHAT.4.1v1 | External chat restricted to allowlisted domains | 2.1 |
| GWS.CHAT.5.1v1 | Content reporting enabled for all conversation types | 3.1 |
| GWS.CHAT.5.2v1 | All reporting categories selected | 3.1 |
Machine-checking these baselines. Five of the seven map onto Policy API fields, so they can be asserted continuously rather than sampled at audit time (3.4):
| SCuBA Baseline | Policy API field to assert |
|---|---|
| GWS.CHAT.1.1v1 | chat.chat_history → history_on_by_default = true |
| GWS.CHAT.1.2v1 | chat.chat_history → allow_user_modification = false |
| GWS.CHAT.2.1v1 | chat.chat_file_sharing → external_file_sharing = NO_FILES |
| GWS.CHAT.3.1v1 | chat.space_history → history_state = HISTORY_ALWAYS_ON |
| GWS.CHAT.4.1v1 | chat.external_chat_restriction → external_chat_restriction = TRUSTED_DOMAINS (or allow_external_chat = false) |
| GWS.CHAT.5.1v1 / 5.2v1 | No Policy API field — content-reporting configuration is verified in the console |
SOC 2 / NIST 800-53 Summary
| Control | SOC 2 | NIST 800-53 |
|---|---|---|
| 1.1 Chat apps | CC6.1 | CM-7 |
| 2.1 External chat | CC6.6 | AC-20 |
| 2.2 File sharing | CC6.6 | AC-3 |
| 2.3 History & retention | CC7.2 | AU-9 |
| 2.4 Auto-deletion retention | CC6.5 | AU-11, SI-12 |
| 2.5 DLP for Chat | CC6.7 | AC-4, SI-4 |
| 2.6 Space access default | CC6.1 | AC-3, AC-6 |
| 2.7 Third-party archiving | CC6.7 | AC-4, AU-9 |
| 3.1 Audit & reporting | CC7.2 | AU-6, IR-6 |
| 3.2 Content protection coverage | CC7.1 | SI-3, SI-8 |
| 3.3 Scoped moderator role | CC7.3 | AC-6(5), IR-4 |
| 3.4 Policy API drift detection | CC7.1 | CM-2, CM-3, SI-4 |
Platform-wide compliance mappings (authentication, OAuth, DLP, admin audit logging) are in the Google Workspace guide.
References
- Control external chat and spaces options — Google Workspace Admin Help
- Prevent data leaks from Chat messages and attachments (DLP for Chat) — Google Workspace Admin Help
- Set up content moderation for Chat — Google Workspace Admin Help
- Automatically delete Chat messages for your organization — Google Workspace Admin Help
- Set up space access for Chat — Google Workspace Admin Help
- Chat content protection — Google Workspace Admin Help
- Security checklist for medium and large businesses — Google Workspace Admin Help
- CIS Google Workspace Foundations Benchmark — Center for Internet Security
Automation interfaces (verified 2026-08-12):
- Settings available in the Policy API — the authoritative list of the eight readable
chat.*settings and their exact fields and enum values - Listing and getting policies —
cloudidentity.googleapis.com/v1/policies, filter syntaxsetting.type.matches("chat.*"), scopecloud-identity.policies.readonly - Chat activity events (Admin SDK Reports API) — every Chat event name and its parameters
- Search for and manage spaces as an administrator —
spaces.searchwithuseAdminAccess, admin scopes, and query syntax - Directory API — roles.insert — custom admin role creation for the scoped moderator role
- Set up service log exports to BigQuery — export setup and the Pacific-Time partition boundary
- Example queries for reporting logs in BigQuery — the
activitytable shape these Code Packs query - Google Workspace CLI (
gws) — Google-published, and explicitly “not an officially supported Google product” - Terraform provider for Google Workspace — archived 2025-06-30, read-only; it has never offered a Chat-specific resource
Changelog
| Version | Date | Changes |
|---|---|---|
| 0.3.0 | 2026-08-12 | Corrections: 2.1 previously advised scoping the external spaces / group-DM setting per organizational unit — Google documents that setting as organization-wide with no OU override, so the advice was unachievable as written and is now corrected, with the OU-scopable external-chat setting distinguished from it. Also documented that disabling external chat does not remove existing external conversations or space memberships (they persist and are restored if the setting is re-enabled), which makes the disable a control on new collaboration only. Added the two documented bypasses of 1.1 (Marketplace installation overrides the Chat-specific toggle; unpublished apps can reach a small user set without Marketplace approval despite an allowlist), the extra-large-space exception where Chat content follows space retention regardless of Vault holds, and the fact that only 1:1 DMs support per-OU auto-deletion scoping. Sharpened 2.6 with the shipped default target audience (all users in the domain) and 1.1 with the incoming-webhook URL-secret authentication model. Re-checked the CIS Google Workspace citation question and re-affirmed the deliberate omission with its reason (candidate numbering was reachable only via unofficial mirrors and shifts between benchmark versions). SCuBA policy IDs re-verified against ScubaGoggles: unchanged, all seven still current. Automation-surface currency pass. Cloud Identity Policy API established as a readable interface for eight Chat settings, which reverses this guide’s previous “Chat settings have no API” posture: added 3.4 (continuous drift detection) and verification Code Packs on 1.1, 2.1, 2.2, 2.3, and 2.6, each stating plainly that the API is read-only (Mutate supported: No). Added 2.7 (govern third-party Chat archiving) — a content-egress setting the guide had never covered. Extended 2.6 with org-wide space inventory via the Chat API’s admin spaces.search. Added Code Packs to 3.3 (Directory API custom-role creation, assignment, and privilege-creep audit). Rewrote 3.1’s Code Packs onto the first-party Reports API, moved the CLI variant to Google’s gws CLI and off community-maintained GAM, and expanded the events table from 4 to 13 entries against the Reports API appendix — adding attachment_download, message_deleted/message_edited, room_deleted, remove_room_member, direct_message_started, the app_* family, and the message_reported/message_report_resolved pair that makes moderation-queue throughput measurable. Fleshed out the BigQuery detection layer from one file to three (Chat app abuse at 1.1, evidence tampering at 2.3, exfiltration and queue health at 3.1). Annotated 2.4, 2.5, and 3.2 as genuinely ClickOps-only with the reason, rather than leaving them silently pack-less. Disclosed that the hashicorp/googleworkspace Terraform provider was archived 2025-06-30. Corrected seven stale pre-platform-split control references in the Code Packs. |
| 0.2.1 | 2026-08-08 | Rewrote §3.1 BigQuery detection pack against the documented Workspace activity schema — completed coverage of the control’s event table by adding message_posted volume (with distinct source-IP count) and bulk add_room_member detections alongside the existing attachment_upload and room_created queries; all columns verified against Google’s BigQuery export example-queries documentation. |
| 0.2.0 | 2026-08-08 | Currency pass against Google Workspace admin docs and CISA SCuBA: corrected 3.1 content-reporting edition prerequisites and documented its history-off/external-DM/external-space blind spots; corrected 2.1’s console path and label to the single External chat settings page covering spaces and group DMs, and dropped the unverifiable CIS Google Workspace citation; added 2.4 auto-deletion retention, 2.5 DLP for Chat, 2.6 restricted space access default, 3.2 Chat content protection coverage limits, and 3.3 scoped Chat moderator role. Rebuilt the BigQuery detection pack against the real Workspace activity-export schema (activity table, flat email column, TIMESTAMP_MICROS(time_usec) window). |
| 0.1.0 | 2026-05-29 | Initial Google Chat product guide — split from the Google Workspace guide (controls 1.1 app allowlisting, 2.1 external chat, 2.2 file sharing, 2.3 history & retention, 3.1 audit & content reporting). Part of the multi-product platform restructure. |
Contributing
Found an issue or have an improvement? See the Google Workspace platform guide for platform-wide controls, or open an issue/PR on GitHub.