v0.3.0-draft AI Drafted

Google Chat Hardening Guide

Productivity Last updated: 2026-08-12

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.

View:

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 readingchat.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 — so almost every control in this guide can now be continuously verified in code even though none can be set in code (Mutate supported: No for 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

  1. App & Integration Security
  2. Data Security
  3. Monitoring & Detection
  4. Compliance Quick Reference

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

ClickOps Implementation

Step 1: Restrict Chat App Installation

  1. Navigate to: Admin ConsoleAppsGoogle WorkspaceGoogle ChatChat apps
  2. Set Allow users to install Chat apps to Off (or leave On only if paired with a Marketplace allowlist)
  3. Set Allow users to add and use incoming webhooks to Off for the organization (enable only for a dedicated, audited OU if needed)
  4. 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:

  1. 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.
  2. 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_added and app_invoked in 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

  1. Navigate to: Admin ConsoleAppsGoogle Workspace Marketplace appsApps list
  2. Click Google Workspace Marketplace allowlistAdd app to allowlist
  3. Add only reviewed, business-justified Chat apps
  4. Set the Marketplace settings so users can install allowlisted apps only

Time to Complete: ~30 minutes

Code Implementation

Code Pack: Terraform
hth-google-chat-1.01-restrict-chat-apps.tf View source on GitHub ↗
#
# ⚠ 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
hth-google-chat-1.01-verify-chat-apps-policy.sh View source on GitHub ↗
# 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
hth-google-chat-1.01-bigquery-chat-app-abuse.sql View source on GitHub ↗
-- 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

  1. As a standard user, confirm a non-allowlisted Chat app cannot be installed
  2. Confirm incoming webhook creation is blocked outside the approved OU
  3. Review ReportingAudit and investigationChat 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

  1. Navigate to: Admin ConsoleAppsGoogle WorkspaceGoogle ChatExternal chat settings
  2. 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
  3. Uncheck Auto-accept chat invites from familiar contacts (L2+)
  4. Click Save

Step 2: Restrict External Spaces and Group Direct Messages

  1. Stay on the same page — Admin ConsoleAppsGoogle WorkspaceGoogle ChatExternal chat settings. There is no separate “External spaces” page; the spaces and group-DM setting lives alongside the external-chat setting.
  2. For Allow users to create & join spaces & group direct messages with people outside their organization: select Off, or On restricted to allowlisted domains
  3. 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

  1. The Chat allowlist is the shared Workspace trusted-domains allowlist (also used by Drive, Sites, Classroom, Looker Studio)
  2. Navigate to: AccountDomainsAllowlisted domains to add/remove trusted domains
  3. 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
hth-google-chat-2.01-restrict-external-chat.tf View source on GitHub ↗
#
# ⚠ 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
hth-google-chat-2.01-verify-external-chat-policy.sh View source on GitHub ↗
# 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

  1. As a standard user, attempt to message a non-allowlisted external address—delivery should be blocked
  2. Attempt to add a non-allowlisted external user to a space—should fail
  3. Attempt to start a group direct message including a non-allowlisted external address—should fail
  4. 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

  1. Navigate to: Admin ConsoleAppsGoogle WorkspaceGoogle ChatChat file sharing
  2. Set External filesharing to No files (SCuBA GWS.CHAT.2.1v1)
  3. Set Internal filesharing to Allow all files or, for sensitive OUs, Images only
  4. 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
hth-google-chat-2.02-restrict-chat-filesharing.tf View source on GitHub ↗
#
# ⚠ 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
hth-google-chat-2.02-verify-filesharing-policy.sh View source on GitHub ↗
# 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

  1. As a standard user, attempt to attach a file in an external conversation—should be blocked
  2. Confirm internal sharing behaves per the configured posture
  3. Review Chat log events for attachment_upload activity

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

  1. Navigate to: Admin ConsoleAppsGoogle WorkspaceGoogle ChatHistory for chats
  2. Select History is ON
  3. Uncheck Allow users to change their history setting
  4. Click Save

Step 2: Enforce Space History

  1. Navigate to: AppsGoogle WorkspaceGoogle ChatHistory for spaces
  2. Select History is ALWAYS ON
  3. Click Save

Step 3: Configure Vault Retention & Holds

  1. In Google VaultRetention, create a Chat retention rule by organizational unit or for all spaces; set retention for DMs, group messages, and space messages
  2. In VaultMattersHolds, place relevant accounts/OUs on a Chat hold (include spaces the user belongs to)
  3. 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
hth-google-chat-2.03-verify-history-policy.sh View source on GitHub ↗
# 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
hth-google-chat-2.03-vault-chat-hold.py View source on GitHub ↗
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
hth-google-chat-2.03-bigquery-history-tampering.sql View source on GitHub ↗
-- 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

  1. Confirm a standard user cannot toggle history off in a conversation
  2. Verify the Vault retention rule and hold appear and cover the Chat corpus
  3. 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

  1. Navigate to: Admin ConsoleAppsGoogle WorkspaceGoogle ChatAuto-deletion
  2. Select the organizational unit to configure
  3. 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
  4. 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

  1. Confirm the configured period appears for each of the three conversation types on the target OU
  2. Post a test message in a history-on space, and confirm it is no longer visible in Chat after the retention period elapses
  3. 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 SecurityAccess and data controlData 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

  1. Navigate to: Admin ConsoleSecurityAccess and data controlData protectionManage RulesAdd rule
  2. Name the rule and scope it to the target organizational units or groups
  3. Under the applications/triggers step, select Google Chat, and include message text and attachments in scope
  4. Define the conditions using content detectors that match your sensitive-data categories
  5. Choose an action: Block message, Warn users, or Audit only
  6. 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.send and google.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

  1. From a test account in scope, send a message containing synthetic data matching a rule condition — confirm the configured block or warn behavior fires
  2. Repeat the test in a conversation with history turned off — the rule must still fire, which is the point of this control
  3. Attach an image containing matching content and confirm the attachment is inspected
  4. 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

  1. Navigate to: Admin ConsoleAppsGoogle WorkspaceGoogle ChatSharing settingsSpace access default
  2. Select Restricted — only people and groups explicitly added can join the space
  3. Click Save

Step 2: Review Target Audiences (if used)

  1. Navigate to: Admin ConsoleAppsGoogle WorkspaceGoogle ChatSharing settingsTarget audiences
  2. 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
  3. Order the list so the narrowest appropriate audience sits first — never leave a broad organization-wide audience in the first position
  4. 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

  1. 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
  2. Grant the Manage Chat and Spaces conversation admin privilege to the account that will run the inventory
  3. Use the Chat API’s admin space search (spaces.search with useAdminAccess) 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
  4. 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
hth-google-chat-2.06-gws-audit-space-access.sh View source on GitHub ↗
# 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

  1. As a standard user, create a new space and confirm the access setting defaults to restricted rather than to a target audience
  2. From a second account not added to that space, confirm the space is not discoverable in Chat search
  3. If target audiences are used, confirm the first-position audience is the intended narrow one
  4. 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
  5. Confirm the Policy API reports chat.space_access_default access_type as RESTRICTED (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

  1. Navigate to: Admin ConsoleAppsGoogle WorkspaceGoogle Chat
  2. 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

  1. If archiving is not required, confirm it is disabled
  2. 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
  3. Re-confirm the destination whenever the archiving vendor relationship changes

Code Implementation

Code Pack: API Script
hth-google-chat-2.07-verify-third-party-archiving.sh View source on GitHub ↗
# 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

  1. Read chat.third_party_archiving through the Policy API and confirm enabled matches your documented decision
  2. If enabled, confirm destination_email_address exactly equals the approved address — the check is string equality against a recorded value, not a judgement call
  3. Confirm the configured archival_frequency matches 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

  1. Navigate to: Admin ConsoleReportingAudit and investigationChat log events
  2. Filter by Event (e.g., attachment_upload, room_created) and date range
  3. For advanced triage (Enterprise Standard/Plus): SecuritySecurity centerInvestigation tool, data source Chat log events

Step 2: Enable Content Reporting

  1. Navigate to: AppsGoogle WorkspaceGoogle ChatContent reporting
  2. Enable Allow users to report content in Chat
  3. Select all conversation type checkboxes (1:1, group, spaces) — SCuBA GWS.CHAT.5.1v1
  4. Select all reporting categories — SCuBA GWS.CHAT.5.2v1
  5. 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
hth-google-chat-3.01-chat-audit-events.sh View source on GitHub ↗
# 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
hth-google-chat-3.01-gws-chat-audit-events.sh View source on GitHub ↗
# 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
hth-google-chat-3.01-chat-audit-events.py View source on GitHub ↗
# 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
hth-google-chat-3.01-bigquery-chat-detection.sql View source on GitHub ↗
-- 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

  1. 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)
  2. Submit a test report and confirm it surfaces in the Moderation Tool for the assigned moderator (3.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

  1. Navigate to: Admin ConsoleAppsGoogle WorkspaceGoogle ChatSecurity & moderationChat content protection
  2. Confirm protection is enabled for chats with external users (on by default)
  3. Click Save if any change was made

Step 2: Close the Internal Gap

  1. Record in your detection coverage documentation that internal-only Chat conversations are not scanned for spam or phishing
  2. Ensure a DLP for Chat rule (2.5) covers internal conversations for the sensitive-data categories that matter
  3. 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

  1. Confirm the content protection setting is enabled for external chat
  2. Send a benign test file internally and confirm it is virus-scanned before delivery
  3. 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

  1. Navigate to: Admin ConsoleAccountAdmin rolesCreate new role
  2. Name it (for example, Chat Content Moderator) and add a description naming the triage owner and SLA
  3. In the privileges list, select Moderate Chat content report — and nothing else
  4. Click Create role

Step 2: Assign the Role

  1. Open the new role → AdminsAssign users
  2. Assign only the individuals responsible for triage
  3. Click Assign role

Step 3: Operate the Queue

  1. Direct assigned moderators to the Workspace Moderation Tool to review reported Chat content
  2. Review the queue on the cadence set by your SLA and record dispositions
  3. 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
hth-google-chat-3.03-chat-moderator-role.sh View source on GitHub ↗
# 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

  1. Confirm the custom role lists Moderate Chat content report and no additional privileges
  2. As an assigned moderator, open the Moderation Tool and confirm reported content is visible and actionable
  3. As a non-assigned user, confirm the Moderation Tool is inaccessible
  4. Submit a test report from 3.1 and confirm it reaches the queue and is dispositioned within the SLA
  5. Measure the queue rather than trusting it: compare message_reported against message_report_resolved counts 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, and external_chat_restriction map 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

  1. Create (or reuse) a service account and enable domain-wide delegation for it
  2. Navigate to: Admin ConsoleSecurityAccess and data controlAPI controlsDomain-wide delegationAdd new
  3. Add the service account’s client ID with the scope https://www.googleapis.com/auth/cloud-identity.policies.readonly
  4. Have it impersonate a super administrator — lesser admin roles cannot read policies

Step 2: Schedule the Comparison

  1. Run the posture check against your recorded baseline on a schedule (daily is proportionate; the settings change rarely and drift matters immediately)
  2. 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
hth-google-chat-3.04-policy-api-posture.py View source on GitHub ↗
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

  1. Run the posture check against a known-good tenant and confirm it reports no findings
  2. 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
  3. Confirm a non-super-admin caller is rejected — if a lesser role appears to work, verify what it actually returned rather than assuming coverage
  4. 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_historyhistory_on_by_default = true
GWS.CHAT.1.2v1 chat.chat_historyallow_user_modification = false
GWS.CHAT.2.1v1 chat.chat_file_sharingexternal_file_sharing = NO_FILES
GWS.CHAT.3.1v1 chat.space_historyhistory_state = HISTORY_ALWAYS_ON
GWS.CHAT.4.1v1 chat.external_chat_restrictionexternal_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

Automation interfaces (verified 2026-08-12):

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.