Compare commits

..

12 Commits

Author SHA1 Message Date
Pablo F.G fb97f6548a ci: allow nodejs.org in renovate validator workflow
prek provisions a Node.js runtime for the renovate-config-validator
hook (which is node-based) and downloads the binary from nodejs.org.
Adds the host to the harden-runner allow-list.
2026-05-14 17:25:42 +02:00
Pablo F.G 87ec00c200 ci: allow registry.npmjs.org in renovate validator workflow
The renovate-config-validator hook (renovatebot/pre-commit-hooks) is
node-based and resolves its npm package set against registry.npmjs.org
during the prek setup phase. Adds the host to the harden-runner
allow-list alongside the GitHub and PyPI endpoints.
2026-05-14 17:25:42 +02:00
Pablo F.G 5aeaf309e9 ci: allow release-assets.githubusercontent.com in renovate validator workflow
setup-uv downloads the uv binary from GitHub release assets; the
harden-runner egress block was rejecting it. Adds the host to the
allow-list alongside the other GitHub endpoints.
2026-05-14 17:25:42 +02:00
Pablo F.G c59d893879 chore(ci): exempt vulnerability alerts from Renovate rate caps 2026-05-14 17:25:42 +02:00
Pablo F.G 9f27b77d7a chore(ci): add Renovate baseline config 2026-05-14 17:25:42 +02:00
Pablo F.G 84332cb74a ci: added renovate config check workflow 2026-05-14 17:25:42 +02:00
Pablo F.G 1352b8ae84 chore: add renovate config check to pre commit 2026-05-14 17:25:42 +02:00
Pablo F.G 445f1e5796 chore(worktree): include openspec/ in copy-ignored allowlist
Ensures `wt switch --create` copies the nested openspec/ clone
into new worktrees alongside .envrc and ui/.env.local, so each
branch can carry its own openspec state.
2026-05-14 17:25:42 +02:00
lydiavilchez 9894ac7bc3 feat(googleworkspace): implement Chat service with 6 CIS checks (#11126) 2026-05-14 17:19:11 +02:00
Pedro Martín 8ae076f22c fix(gcp): update duplicated CIS requirement ID (#11180) 2026-05-14 16:17:04 +02:00
Adrián Peña 27fb8518cd chore(sdk): pin root transitive deps to prevent silent drift (#11178) 2026-05-14 16:14:34 +02:00
Pepe Fagoaga 375de5dc87 docs: replace safety for osv-scanner (#11179) 2026-05-14 15:04:09 +02:00
43 changed files with 2653 additions and 66 deletions
+158
View File
@@ -0,0 +1,158 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:best-practices",
":enablePreCommit",
":semanticCommits",
":enableVulnerabilityAlertsWithLabel(security)",
"docker:enableMajor",
"helpers:pinGitHubActionDigestsToSemver",
"helpers:disableTypesNodeMajor",
"security:openssf-scorecard",
"customManagers:githubActionsVersions",
"customManagers:dockerfileVersions"
],
"timezone": "Europe/Madrid",
"baseBranches": [
"master"
],
"labels": [
"dependencies"
],
"dependencyDashboardTitle": "Dependency Dashboard",
"prConcurrentLimit": 10,
"prHourlyLimit": 2,
"vulnerabilityAlerts": {
"prHourlyLimit": 0,
"prConcurrentLimit": 0
},
"configMigration": true,
"minimumReleaseAge": "7 days",
"rangeStrategy": "pin",
"packageRules": [
{
"description": "Patches: 1st of every month, Madrid overnight window (22:00-06:00)",
"matchUpdateTypes": [
"patch"
],
"schedule": [
"* 22-23,0-5 1 * *"
]
},
{
"description": "Minors: 8th of every 2 months, Madrid overnight window - staggered one week from patches",
"matchUpdateTypes": [
"minor"
],
"schedule": [
"* 22-23,0-5 8 */2 *"
]
},
{
"description": "Majors: 15th of every 3 months, Madrid overnight window - staggered two weeks from patches",
"matchUpdateTypes": [
"major"
],
"schedule": [
"* 22-23,0-5 15 */3 *"
]
},
{
"description": "GitHub Actions - single grouped PR, no changelog, scope=ci",
"matchManagers": [
"github-actions"
],
"groupName": "github-actions",
"semanticCommitScope": "ci",
"addLabels": [
"no-changelog"
]
},
{
"description": "Docker images - single grouped PR, no changelog, scope=docker",
"matchManagers": [
"dockerfile",
"docker-compose"
],
"groupName": "docker",
"semanticCommitScope": "docker",
"addLabels": [
"no-changelog"
]
},
{
"description": "Pre-commit hooks - single grouped PR, scope=pre-commit",
"matchManagers": [
"pre-commit"
],
"groupName": "pre-commit hooks",
"semanticCommitScope": "pre-commit",
"addLabels": [
"no-changelog"
]
},
{
"description": "Helm charts - single grouped PR, scope=helm",
"matchManagers": [
"helmv3"
],
"groupName": "helm charts",
"semanticCommitScope": "helm",
"addLabels": [
"no-changelog",
"helm"
]
},
{
"description": "UI - scope=ui",
"matchFileNames": [
"ui/**"
],
"semanticCommitScope": "ui"
},
{
"description": "API - scope=api",
"matchFileNames": [
"api/**"
],
"semanticCommitScope": "api"
},
{
"description": "MCP server - scope=mcp",
"matchFileNames": [
"mcp_server/**"
],
"semanticCommitScope": "mcp"
},
{
"description": "Python SDK (root) - scope=sdk",
"matchFileNames": [
"pyproject.toml",
"poetry.lock",
"util/prowler-bulk-provisioning/**"
],
"semanticCommitScope": "sdk"
},
{
"description": "Lockfile maintenance - no changelog (scope inherits from component rules above)",
"matchUpdateTypes": [
"lockFileMaintenance"
],
"addLabels": [
"no-changelog"
]
},
{
"description": "UI devDependencies - no changelog",
"matchFileNames": [
"ui/**"
],
"matchDepTypes": [
"devDependencies"
],
"addLabels": [
"no-changelog"
]
}
]
}
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
api.github.com:443
- name: Comment and lock issue
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const { owner, repo } = context.repo;
+24 -24
View File
@@ -75,7 +75,7 @@ jobs:
with:
destination: /opt/gh-aw/actions
- name: Check workflow file timestamps
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_AW_WORKFLOW_FILE: "issue-triage.lock.yml"
with:
@@ -86,7 +86,7 @@ jobs:
await main();
- name: Compute current body text
id: compute-text
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
@@ -96,7 +96,7 @@ jobs:
- name: Add comment with workflow run link
id: add-comment
if: github.event_name == 'issues' || github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment' || github.event_name == 'discussion' || github.event_name == 'discussion_comment' || (github.event_name == 'pull_request') && (github.event.pull_request.head.repo.id == github.repository_id)
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_AW_WORKFLOW_NAME: "Issue Triage"
GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e 🤖 Generated by [Prowler Issue Triage]({run_url}) [Experimental]\"}"
@@ -148,7 +148,7 @@ jobs:
with:
persist-credentials: false
- name: Merge remote .github folder
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_AW_AGENT_FILE: ".github/agents/issue-triage.md"
GH_AW_AGENT_IMPORT_SPEC: "../agents/issue-triage.md"
@@ -175,7 +175,7 @@ jobs:
id: checkout-pr
if: |
github.event.pull_request
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
with:
@@ -187,7 +187,7 @@ jobs:
await main();
- name: Generate agentic run info
id: generate_aw_info
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const fs = require('fs');
@@ -511,7 +511,7 @@ jobs:
}
GH_AW_MCP_CONFIG_EOF
- name: Generate workflow overview
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs');
@@ -606,7 +606,7 @@ jobs:
{{#runtime-import .github/workflows/issue-triage.md}}
GH_AW_PROMPT_EOF
- name: Substitute placeholders
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
GH_AW_GITHUB_ACTOR: ${{ github.actor }}
@@ -640,7 +640,7 @@ jobs:
}
});
- name: Interpolate variables and render templates
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
@@ -757,7 +757,7 @@ jobs:
bash /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID"
- name: Redact secrets in logs
if: always()
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
@@ -779,7 +779,7 @@ jobs:
if-no-files-found: warn
- name: Ingest agent output
id: collect_output
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }}
GH_AW_ALLOWED_DOMAINS: "*.pythonhosted.org,anaconda.org,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,binstar.org,bootstrap.pypa.io,conda.anaconda.org,conda.binstar.org,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,files.pythonhosted.org,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,mcp.context7.com,mcp.prowler.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pip.pypa.io,ppa.launchpad.net,pypi.org,pypi.python.org,raw.githubusercontent.com,registry.npmjs.org,repo.anaconda.com,repo.continuum.io,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com"
@@ -808,7 +808,7 @@ jobs:
if-no-files-found: ignore
- name: Parse agent logs for step summary
if: always()
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/
with:
@@ -819,7 +819,7 @@ jobs:
await main();
- name: Parse MCP gateway logs for step summary
if: always()
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
@@ -891,7 +891,7 @@ jobs:
echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV"
- name: Process No-Op Messages
id: noop
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
GH_AW_NOOP_MAX: 1
@@ -905,7 +905,7 @@ jobs:
await main();
- name: Record Missing Tool
id: missing_tool
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
GH_AW_WORKFLOW_NAME: "Issue Triage"
@@ -918,7 +918,7 @@ jobs:
await main();
- name: Handle Agent Failure
id: handle_agent_failure
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
GH_AW_WORKFLOW_NAME: "Issue Triage"
@@ -937,7 +937,7 @@ jobs:
await main();
- name: Handle No-Op Message
id: handle_noop_message
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
GH_AW_WORKFLOW_NAME: "Issue Triage"
@@ -954,7 +954,7 @@ jobs:
await main();
- name: Update reaction comment with completion status
id: conclusion
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }}
@@ -1008,7 +1008,7 @@ jobs:
run: |
echo "Agent output-types: $AGENT_OUTPUT_TYPES"
- name: Setup threat detection
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
WORKFLOW_NAME: "Issue Triage"
WORKFLOW_DESCRIPTION: "[Experimental] AI-powered issue triage for Prowler - produces coding-agent-ready fix plans"
@@ -1062,7 +1062,7 @@ jobs:
XDG_CONFIG_HOME: /home/runner
- name: Parse threat detection results
id: parse_results
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
@@ -1102,7 +1102,7 @@ jobs:
- name: Add eyes reaction for immediate feedback
id: react
if: github.event_name == 'issues' || github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment' || github.event_name == 'discussion' || github.event_name == 'discussion_comment' || (github.event_name == 'pull_request') && (github.event.pull_request.head.repo.id == github.repository_id)
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_AW_REACTION: "eyes"
with:
@@ -1114,7 +1114,7 @@ jobs:
await main();
- name: Check team membership for workflow
id: check_membership
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_AW_REQUIRED_ROLES: admin,maintainer,write
with:
@@ -1126,7 +1126,7 @@ jobs:
await main();
- name: Check user rate limit
id: check_rate_limit
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_AW_RATE_LIMIT_MAX: "5"
GH_AW_RATE_LIMIT_WINDOW: "60"
@@ -1185,7 +1185,7 @@ jobs:
echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV"
- name: Process Safe Outputs
id: process_safe_outputs
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":1},\"missing_data\":{},\"missing_tool\":{}}"
@@ -0,0 +1,57 @@
name: 'CI: Renovate Config Validate'
on:
pull_request:
branches:
- 'master'
paths:
- '.github/renovate.json'
- '.pre-commit-config.yaml'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions: {}
env:
# renovate: datasource=pypi depName=prek
PREK_VERSION: '0.4.0'
jobs:
validate:
name: Validate Renovate config
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
with:
egress-policy: block
allowed-endpoints: >
api.github.com:443
github.com:443
objects.githubusercontent.com:443
codeload.github.com:443
release-assets.githubusercontent.com:443
pypi.org:443
files.pythonhosted.org:443
registry.npmjs.org:443
nodejs.org:443
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up uv
uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1
- name: Install prek
run: uv tool install "prek==${PREK_VERSION}"
- name: Validate Renovate config
run: prek run renovate-config-validator --files .github/renovate.json
+8
View File
@@ -49,6 +49,14 @@ repos:
files: ^\.github/(workflows|actions)/.+\.ya?ml$|^\.github/dependabot\.ya?ml$
priority: 30
## RENOVATE
- repo: https://github.com/renovatebot/pre-commit-hooks
rev: 43.150.0
hooks:
- id: renovate-config-validator
files: ^\.github/renovate\.json$
priority: 10
## BASH
- repo: https://github.com/koalaman/shellcheck-precommit
rev: v0.11.0
+1
View File
@@ -1,2 +1,3 @@
.envrc
ui/.env.local
openspec/
+1 -1
View File
@@ -152,7 +152,7 @@ These should have been already installed if `uv sync` was already run.
</Note>
- [`bandit`](https://pypi.org/project/bandit/) for code security review.
- [`safety`](https://pypi.org/project/safety/) and [`dependabot`](https://github.com/features/security) for dependencies.
- [`osv-scanner`](https://github.com/google/osv-scanner) and [`dependabot`](https://github.com/features/security) for dependencies.
- [`hadolint`](https://github.com/hadolint/hadolint) and [`dockle`](https://github.com/goodwithtech/dockle) for container security.
- [`Snyk`](https://docs.snyk.io/integrations/snyk-container-integrations/container-security-with-docker-hub-integration) for container security in Docker Hub.
- [`clair`](https://github.com/quay/clair) for container security in Amazon ECR.
+5 -4
View File
@@ -39,10 +39,11 @@ Dependencies are continuously monitored for known vulnerabilities with timely up
### Dependency Vulnerability Scanning
- **Safety:** Scans Python dependencies against known vulnerability databases
- Runs on every commit via pre-commit hooks
- Integrated into CI/CD for SDK and API
- Configured with selective ignores for tracked exceptions
- **osv-scanner:** Scans lockfiles against the [OSV.dev](https://osv.dev) vulnerability database
- Runs in CI on every pull request and push for SDK, API, and UI
- Fails the build on `HIGH`, `CRITICAL`, and `UNKNOWN` severity findings
- Posts a per-lockfile report as a PR comment
- Per-vulnerability ignores (with reason and expiry) live in `osv-scanner.toml` at the repo root
- **Trivy:** Multi-purpose scanner for containers and dependencies
- Scans all container images (UI, API, SDK, MCP Server)
- Checks for vulnerabilities in OS packages and application dependencies
@@ -18,7 +18,7 @@ Prowler requests the following read-only OAuth 2.0 scopes:
| `https://www.googleapis.com/auth/admin.directory.domain.readonly` | Read access to domain information |
| `https://www.googleapis.com/auth/admin.directory.customer.readonly` | Read access to customer information (Customer ID) |
| `https://www.googleapis.com/auth/admin.directory.orgunit.readonly` | Read access to organizational unit hierarchy (identifies the root OU for policy filtering) |
| `https://www.googleapis.com/auth/cloud-identity.policies.readonly` | Read access to domain-level application policies (required for Calendar service checks) |
| `https://www.googleapis.com/auth/cloud-identity.policies.readonly` | Read access to domain-level application policies (required for Calendar, Gmail, Chat, and Drive service checks) |
| `https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly` | Read access to admin roles and role assignments |
<Warning>
@@ -40,7 +40,7 @@ In the [Google Cloud Console](https://console.cloud.google.com), select the targ
| API | Required For |
|-----|--------------|
| **Admin SDK API** | Directory service checks (users, roles, domains) |
| **Cloud Identity API** | Calendar service checks (domain-level sharing and invitation policies) |
| **Cloud Identity API** | Calendar, Gmail, Chat, and Drive service checks (domain-level application policies) |
For each API:
@@ -49,7 +49,7 @@ For each API:
3. Click **Enable**
<Note>
Both APIs must be enabled in the same GCP project that hosts the Service Account. Calendar checks will return no findings if the Cloud Identity API is not enabled.
Both APIs must be enabled in the same GCP project that hosts the Service Account. Calendar, Gmail, Chat, and Drive checks will return no findings if the Cloud Identity API is not enabled.
</Note>
### Step 3: Create a Service Account
@@ -176,9 +176,9 @@ If Prowler connects but returns empty results or permission errors for specific
- Verify all scopes are authorized in the Admin Console
- Ensure the delegated user is an active super administrator
### Calendar Checks Return No Findings
### Policy API Checks Return No Findings
If the Directory checks run successfully but the Calendar checks (e.g., `calendar_external_sharing_primary_calendar`) return no findings, the Cloud Identity Policy API is not reachable for this Service Account. Verify:
If the Directory checks run successfully but the Calendar, Gmail, Chat, or Drive checks return no findings, the Cloud Identity Policy API is not reachable for this Service Account. Verify:
- The **Cloud Identity API** is enabled in the GCP project hosting the Service Account (Step 2)
- The scope `https://www.googleapis.com/auth/cloud-identity.policies.readonly` is included in the Domain-Wide Delegation OAuth scopes list in the Admin Console (Step 5)
+2
View File
@@ -6,6 +6,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
### 🚀 Added
- 6 Chat file sharing, external messaging, spaces, and apps access checks for Google Workspace provider using the Cloud Identity Policy API [(#11126)](https://github.com/prowler-cloud/prowler/pull/11126)
- `entra_service_principal_no_secrets_for_permanent_tier0_roles` check for M365 provider [(#10788)](https://github.com/prowler-cloud/prowler/pull/10788)
- `iam_user_access_not_stale_to_sagemaker` check for AWS provider with configurable `max_unused_sagemaker_access_days` (default 90) [(#11000)](https://github.com/prowler-cloud/prowler/pull/11000)
- `cloudtrail_bedrock_logging_enabled` check for AWS provider [(#10858)](https://github.com/prowler-cloud/prowler/pull/10858)
@@ -31,6 +32,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
### 🐞 Fixed
- `entra_users_mfa_capable` and `entra_break_glass_account_fido2_security_key_registered` report a preventive FAIL per affected user (with the missing permission named) when the M365 service principal lacks `AuditLog.Read.All`, instead of mass false positives [(#10907)](https://github.com/prowler-cloud/prowler/pull/10907)
- Update duplicated GCP CIS requirements IDs [(#11180)](https://github.com/prowler-cloud/prowler/pull/11180)
---
+2 -2
View File
@@ -914,7 +914,7 @@
]
},
{
"Id": "3.1",
"Id": "3.10",
"Description": "Use Identity Aware Proxy (IAP) to Ensure Only Traffic From Google IP Addresses are 'Allowed'",
"Checks": [],
"Attributes": [
@@ -1132,7 +1132,7 @@
]
},
{
"Id": "4.1",
"Id": "4.10",
"Description": "Ensure That App Engine Applications Enforce HTTPS Connections",
"Checks": [],
"Attributes": [
@@ -1084,7 +1084,9 @@
{
"Id": "3.1.4.1.1",
"Description": "Ensure external filesharing in Google Chat and Hangouts is disabled",
"Checks": [],
"Checks": [
"chat_external_file_sharing_disabled"
],
"Attributes": [
{
"Section": "3 Apps",
@@ -1105,7 +1107,9 @@
{
"Id": "3.1.4.1.2",
"Description": "Ensure internal filesharing in Google Chat and Hangouts is disabled",
"Checks": [],
"Checks": [
"chat_internal_file_sharing_disabled"
],
"Attributes": [
{
"Section": "3 Apps",
@@ -1126,7 +1130,9 @@
{
"Id": "3.1.4.2.1",
"Description": "Ensure Google Chat externally is restricted to allowed domains",
"Checks": [],
"Checks": [
"chat_external_messaging_restricted"
],
"Attributes": [
{
"Section": "3 Apps",
@@ -1147,7 +1153,9 @@
{
"Id": "3.1.4.3.1",
"Description": "Ensure external spaces in Google Chat and Hangouts are restricted",
"Checks": [],
"Checks": [
"chat_external_spaces_restricted"
],
"Attributes": [
{
"Section": "3 Apps",
@@ -1168,7 +1176,9 @@
{
"Id": "3.1.4.4.1",
"Description": "Ensure allow users to install Chat apps is disabled",
"Checks": [],
"Checks": [
"chat_apps_installation_disabled"
],
"Attributes": [
{
"Section": "3 Apps",
@@ -1189,7 +1199,9 @@
{
"Id": "3.1.4.4.2",
"Description": "Ensure allow users to add and use incoming webhooks is disabled",
"Checks": [],
"Checks": [
"chat_incoming_webhooks_disabled"
],
"Attributes": [
{
"Section": "3 Apps",
@@ -1466,7 +1466,9 @@
{
"Id": "GWS.CHAT.2.1",
"Description": "External file sharing SHALL be disabled to protect sensitive information from unauthorized or accidental sharing",
"Checks": [],
"Checks": [
"chat_external_file_sharing_disabled"
],
"Attributes": [
{
"Section": "Chat",
@@ -1492,7 +1494,9 @@
{
"Id": "GWS.CHAT.4.1",
"Description": "External chat messaging SHALL be restricted to allowlisted domains only",
"Checks": [],
"Checks": [
"chat_external_messaging_restricted"
],
"Attributes": [
{
"Section": "Chat",
@@ -0,0 +1,39 @@
{
"Provider": "googleworkspace",
"CheckID": "chat_apps_installation_disabled",
"CheckTitle": "Chat apps installation is disabled for users",
"CheckType": [],
"ServiceName": "chat",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "collaboration",
"Description": "Google Chat apps connect to external services to look up information, schedule meetings, or complete tasks. Apps are accounts created by Google, users in the organization, or third parties that can access user data including **email addresses**, **conversation content**, and **organizational information**.",
"Risk": "Unrestricted Chat app installation allows **unvetted third-party applications** to access user data including conversation content and organizational information. An attacker could distribute a malicious Chat app to **exfiltrate confidential data** or establish **persistent access** to internal communications.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://support.google.com/a/answer/6089179",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Google Chat and classic Hangouts**\n3. Click **Chat apps**\n4. Under Chat apps access settings, set **Allow users to install Chat apps** to **OFF**\n5. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Disable Chat apps installation to prevent **unvetted third-party applications** from accessing organizational data through the Chat platform.",
"Url": "https://hub.prowler.com/check/chat_apps_installation_disabled"
}
},
"Categories": [
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [
"chat_incoming_webhooks_disabled"
],
"Notes": ""
}
@@ -0,0 +1,52 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.chat.chat_client import chat_client
class chat_apps_installation_disabled(Check):
"""Check that users cannot install Chat apps.
This check verifies that the domain-level Chat policy prevents users
from installing Chat apps, reducing the risk of data exposure through
third-party or unvetted applications.
"""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if chat_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=chat_client.policies,
resource_id="chatPolicies",
resource_name="Chat Policies",
customer_id=chat_client.provider.identity.customer_id,
)
apps_enabled = chat_client.policies.enable_apps
if apps_enabled is False:
report.status = "PASS"
report.status_extended = (
f"Chat apps installation is disabled "
f"in domain {chat_client.provider.identity.domain}."
)
elif apps_enabled is None:
report.status = "PASS"
report.status_extended = (
f"Chat apps installation uses Google's secure default "
f"configuration (disabled) "
f"in domain {chat_client.provider.identity.domain}."
)
else:
report.status = "FAIL"
report.status_extended = (
f"Chat apps installation is enabled "
f"in domain {chat_client.provider.identity.domain}. "
f"Chat apps installation should be disabled to prevent unvetted apps."
)
findings.append(report)
return findings
@@ -0,0 +1,4 @@
from prowler.providers.common.provider import Provider
from prowler.providers.googleworkspace.services.chat.chat_service import Chat
chat_client = Chat(Provider.get_global_provider())
@@ -0,0 +1,40 @@
{
"Provider": "googleworkspace",
"CheckID": "chat_external_file_sharing_disabled",
"CheckTitle": "External file sharing in Chat is set to no files",
"CheckType": [],
"ServiceName": "chat",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "collaboration",
"Description": "Google Chat **external file sharing** controls whether users can share files with people outside the organization via Chat conversations. Files often contain **confidential information**, and organizations in regulated industries need to control the flow of this information outside their boundaries.",
"Risk": "Enabled external file sharing allows users to send files containing **confidential information** to external parties through Chat. This creates a **data leakage** channel that bypasses DLP controls, particularly dangerous for organizations handling **regulated data** such as PII, PHI, or financial records.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://support.google.com/a/answer/9540647",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Google Chat and classic Hangouts**\n3. Click **Chat File Sharing**\n4. Under Setting, set **External filesharing** to **No files**\n5. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Disable **external file sharing** in Chat to prevent users from sharing files with people outside the organization through Chat conversations.",
"Url": "https://hub.prowler.com/check/chat_external_file_sharing_disabled"
}
},
"Categories": [
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [
"chat_internal_file_sharing_disabled",
"drive_sharing_allowlisted_domains"
],
"Notes": ""
}
@@ -0,0 +1,52 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.chat.chat_client import chat_client
class chat_external_file_sharing_disabled(Check):
"""Check that external file sharing in Google Chat is disabled.
This check verifies that the domain-level Chat policy prevents users
from sharing files with people outside the organization via Chat,
protecting sensitive information from unauthorized external access.
"""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if chat_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=chat_client.policies,
resource_id="chatPolicies",
resource_name="Chat Policies",
customer_id=chat_client.provider.identity.customer_id,
)
external_sharing = chat_client.policies.external_file_sharing
if external_sharing == "NO_FILES":
report.status = "PASS"
report.status_extended = (
f"External file sharing in Chat is disabled "
f"in domain {chat_client.provider.identity.domain}."
)
else:
report.status = "FAIL"
if external_sharing is None:
report.status_extended = (
f"External file sharing in Chat is not explicitly configured "
f"in domain {chat_client.provider.identity.domain}. "
f"External file sharing should be set to No files."
)
else:
report.status_extended = (
f"External file sharing in Chat is set to {external_sharing} "
f"in domain {chat_client.provider.identity.domain}. "
f"External file sharing should be set to No files."
)
findings.append(report)
return findings
@@ -0,0 +1,40 @@
{
"Provider": "googleworkspace",
"CheckID": "chat_external_messaging_restricted",
"CheckTitle": "External Chat messaging is restricted to allowed domains",
"CheckType": [],
"ServiceName": "chat",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "collaboration",
"Description": "Google Chat **external messaging** controls whether users can send messages to people outside the organization. If external messaging is allowed, it can optionally be restricted to only **allowlisted domains** to limit the scope of external communication.",
"Risk": "Unrestricted external messaging allows users to communicate freely with **any external party**, increasing the risk of **data exfiltration** through conversation content and **social engineering attacks** from untrusted domains targeting internal users.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://support.google.com/a/answer/9540647",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Google Chat and classic Hangouts**\n3. Click **External Chat Settings**\n4. Select **Chat externally**\n5. Set **Allow users to send messages outside the organization** to **ON**\n6. Check **Only allow this for allowlisted domains**\n7. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Restrict **external Chat messaging** to **allowlisted domains** only to limit information flow to trusted parties and reduce exposure to external threats.",
"Url": "https://hub.prowler.com/check/chat_external_messaging_restricted"
}
},
"Categories": [
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [
"chat_external_spaces_restricted",
"drive_sharing_allowlisted_domains"
],
"Notes": ""
}
@@ -0,0 +1,59 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.chat.chat_client import chat_client
class chat_external_messaging_restricted(Check):
"""Check that external Chat messaging is restricted to allowed domains.
This check verifies that external Chat messaging is either disabled
entirely or restricted to allowlisted domains only, preventing
unrestricted communication with external users.
"""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if chat_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=chat_client.policies,
resource_id="chatPolicies",
resource_name="Chat Policies",
customer_id=chat_client.provider.identity.customer_id,
)
allow_external = chat_client.policies.allow_external_chat
restriction = chat_client.policies.external_chat_restriction
if allow_external is False:
report.status = "PASS"
report.status_extended = (
f"External Chat messaging is disabled "
f"in domain {chat_client.provider.identity.domain}."
)
elif allow_external is None and restriction is None:
report.status = "PASS"
report.status_extended = (
f"External Chat messaging uses Google's secure default "
f"configuration (disabled) "
f"in domain {chat_client.provider.identity.domain}."
)
elif restriction == "TRUSTED_DOMAINS":
report.status = "PASS"
report.status_extended = (
f"External Chat messaging is restricted to allowed domains "
f"in domain {chat_client.provider.identity.domain}."
)
else:
report.status = "FAIL"
report.status_extended = (
f"External Chat messaging is not restricted to allowed domains "
f"in domain {chat_client.provider.identity.domain}. "
f"External messaging should be restricted to allowed domains only."
)
findings.append(report)
return findings
@@ -0,0 +1,40 @@
{
"Provider": "googleworkspace",
"CheckID": "chat_external_spaces_restricted",
"CheckTitle": "External spaces in Chat are restricted to allowed domains",
"CheckType": [],
"ServiceName": "chat",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "collaboration",
"Description": "Google Chat **external spaces** allow users to create or join collaborative spaces that include people outside the organization. If external spaces are allowed, they can optionally be restricted to only **allowlisted domains** to limit external participation.",
"Risk": "Unrestricted external spaces allow users to add **anyone from any domain** to persistent group conversations. This increases the risk of **confidential information exposure** in shared spaces and enables **unauthorized external access** to ongoing organizational discussions.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://support.google.com/a/answer/9540647",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Google Chat and classic Hangouts**\n3. Click **External Spaces**\n4. Set **Allow users to create and join spaces with people outside their organization** to **ON**\n5. Check **Only allow users to add people from allowlisted domains**\n6. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Restrict **external spaces** to **allowlisted domains** only to control which external parties can participate in organizational Chat spaces.",
"Url": "https://hub.prowler.com/check/chat_external_spaces_restricted"
}
},
"Categories": [
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [
"chat_external_messaging_restricted",
"drive_sharing_allowlisted_domains"
],
"Notes": ""
}
@@ -0,0 +1,59 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.chat.chat_client import chat_client
class chat_external_spaces_restricted(Check):
"""Check that external spaces in Google Chat are restricted.
This check verifies that external spaces are either disabled entirely
or restricted to allowlisted domains only, preventing users from
creating or joining spaces with unrestricted external participants.
"""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if chat_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=chat_client.policies,
resource_id="chatPolicies",
resource_name="Chat Policies",
customer_id=chat_client.provider.identity.customer_id,
)
spaces_enabled = chat_client.policies.external_spaces_enabled
allowlist_mode = chat_client.policies.external_spaces_domain_allowlist_mode
if spaces_enabled is False:
report.status = "PASS"
report.status_extended = (
f"External spaces are disabled "
f"in domain {chat_client.provider.identity.domain}."
)
elif allowlist_mode == "TRUSTED_DOMAINS":
report.status = "PASS"
report.status_extended = (
f"External spaces are restricted to allowed domains "
f"in domain {chat_client.provider.identity.domain}."
)
else:
report.status = "FAIL"
if spaces_enabled is None and allowlist_mode is None:
report.status_extended = (
f"External spaces restriction is not explicitly configured "
f"in domain {chat_client.provider.identity.domain}. "
f"External spaces should be restricted to allowed domains only."
)
else:
report.status_extended = (
f"External spaces are not restricted to allowed domains "
f"in domain {chat_client.provider.identity.domain}. "
f"External spaces should be restricted to allowed domains only."
)
findings.append(report)
return findings
@@ -0,0 +1,39 @@
{
"Provider": "googleworkspace",
"CheckID": "chat_incoming_webhooks_disabled",
"CheckTitle": "Incoming webhooks in Chat are disabled for users",
"CheckType": [],
"ServiceName": "chat",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "collaboration",
"Description": "**Incoming webhooks** let external applications post asynchronous messages into Google Chat spaces without being a Chat app. When enabled, users can configure webhooks and developers can call them to send content from **external applications**.",
"Risk": "Exposed webhook URLs allow **unauthorized content injection** into Chat spaces. Attackers can send **fraudulent or misleading messages** that appear to come from trusted services, creating a vector for **social engineering** and **phishing** within internal communications.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://support.google.com/a/answer/6089179",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Google Chat and classic Hangouts**\n3. Click **Chat apps**\n4. Under Chat apps access settings, set **Allow users to add and use incoming webhooks** to **OFF**\n5. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Disable **incoming webhooks** to prevent unauthenticated external applications from **injecting content** into internal Chat spaces.",
"Url": "https://hub.prowler.com/check/chat_incoming_webhooks_disabled"
}
},
"Categories": [
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [
"chat_apps_installation_disabled"
],
"Notes": ""
}
@@ -0,0 +1,52 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.chat.chat_client import chat_client
class chat_incoming_webhooks_disabled(Check):
"""Check that incoming webhooks are disabled in Google Chat.
This check verifies that the domain-level Chat policy prevents users
from adding and using incoming webhooks, reducing the risk of
unauthorized content being posted into Chat spaces.
"""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if chat_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=chat_client.policies,
resource_id="chatPolicies",
resource_name="Chat Policies",
customer_id=chat_client.provider.identity.customer_id,
)
webhooks_enabled = chat_client.policies.enable_webhooks
if webhooks_enabled is False:
report.status = "PASS"
report.status_extended = (
f"Incoming webhooks are disabled "
f"in domain {chat_client.provider.identity.domain}."
)
elif webhooks_enabled is None:
report.status = "PASS"
report.status_extended = (
f"Incoming webhooks use Google's secure default "
f"configuration (disabled) "
f"in domain {chat_client.provider.identity.domain}."
)
else:
report.status = "FAIL"
report.status_extended = (
f"Incoming webhooks are enabled "
f"in domain {chat_client.provider.identity.domain}. "
f"Incoming webhooks should be disabled to prevent unauthorized content."
)
findings.append(report)
return findings
@@ -0,0 +1,39 @@
{
"Provider": "googleworkspace",
"CheckID": "chat_internal_file_sharing_disabled",
"CheckTitle": "Internal file sharing in Chat is set to no files",
"CheckType": [],
"ServiceName": "chat",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "low",
"ResourceType": "NotDefined",
"ResourceGroup": "collaboration",
"Description": "Google Chat **internal file sharing** controls whether users can share files with other people inside the organization via Chat conversations. Organizations in regulated industries may need to **control and audit** all file sharing, even between internal users.",
"Risk": "Unrestricted internal file sharing in Chat allows files with **sensitive information** to be distributed freely without passing through approved channels. This undermines **data governance** and **audit trail** requirements, making it harder to track data movement within the organization.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://support.google.com/a/answer/9540647",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Google Chat and classic Hangouts**\n3. Click **Chat File Sharing**\n4. Under Setting, set **Internal filesharing** to **No files**\n5. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Disable **internal file sharing** in Chat to enforce file distribution through **approved channels** with proper audit trails and governance controls.",
"Url": "https://hub.prowler.com/check/chat_internal_file_sharing_disabled"
}
},
"Categories": [
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [
"chat_external_file_sharing_disabled"
],
"Notes": ""
}
@@ -0,0 +1,52 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.chat.chat_client import chat_client
class chat_internal_file_sharing_disabled(Check):
"""Check that internal file sharing in Google Chat is disabled.
This check verifies that the domain-level Chat policy prevents users
from sharing files internally via Chat, providing maximum control over
file distribution within the organization.
"""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if chat_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=chat_client.policies,
resource_id="chatPolicies",
resource_name="Chat Policies",
customer_id=chat_client.provider.identity.customer_id,
)
internal_sharing = chat_client.policies.internal_file_sharing
if internal_sharing == "NO_FILES":
report.status = "PASS"
report.status_extended = (
f"Internal file sharing in Chat is disabled "
f"in domain {chat_client.provider.identity.domain}."
)
else:
report.status = "FAIL"
if internal_sharing is None:
report.status_extended = (
f"Internal file sharing in Chat is not explicitly configured "
f"in domain {chat_client.provider.identity.domain}. "
f"Internal file sharing should be set to No files."
)
else:
report.status_extended = (
f"Internal file sharing in Chat is set to {internal_sharing} "
f"in domain {chat_client.provider.identity.domain}. "
f"Internal file sharing should be set to No files."
)
findings.append(report)
return findings
@@ -0,0 +1,125 @@
from typing import Optional
from pydantic import BaseModel
from prowler.lib.logger import logger
from prowler.providers.googleworkspace.lib.service.service import GoogleWorkspaceService
class Chat(GoogleWorkspaceService):
"""Google Workspace Chat service for auditing domain-level Chat policies.
Uses the Cloud Identity Policy API v1 to read Chat file sharing, external
messaging, spaces, and apps access settings configured in the Admin Console.
"""
def __init__(self, provider):
super().__init__(provider)
self.policies = ChatPolicies()
self.policies_fetched = False
self._fetch_chat_policies()
def _fetch_chat_policies(self):
"""Fetch Chat policies from the Cloud Identity Policy API v1."""
logger.info("Chat - Fetching Chat policies...")
try:
service = self._build_service("cloudidentity", "v1")
if not service:
logger.error("Failed to build Cloud Identity service")
return
request = service.policies().list(
pageSize=100,
filter='setting.type.matches("chat.*")',
)
fetch_succeeded = True
while request is not None:
try:
response = request.execute()
for policy in response.get("policies", []):
if not self._is_customer_level_policy(policy):
continue
setting = policy.get("setting", {})
setting_type = setting.get("type", "").removeprefix("settings/")
logger.debug(f"Processing setting type: {setting_type}")
value = setting.get("value", {})
if setting_type == "chat.chat_file_sharing":
self.policies.external_file_sharing = value.get(
"externalFileSharing"
)
self.policies.internal_file_sharing = value.get(
"internalFileSharing"
)
logger.debug("Chat file sharing settings fetched.")
elif setting_type == "chat.external_chat_restriction":
self.policies.allow_external_chat = value.get(
"allowExternalChat"
)
self.policies.external_chat_restriction = value.get(
"externalChatRestriction"
)
logger.debug(
"Chat external chat restriction settings fetched."
)
elif setting_type == "chat.chat_external_spaces":
self.policies.external_spaces_enabled = value.get("enabled")
self.policies.external_spaces_domain_allowlist_mode = (
value.get("domainAllowlistMode")
)
logger.debug("Chat external spaces settings fetched.")
elif setting_type == "chat.chat_apps_access":
self.policies.enable_apps = value.get("enableApps")
self.policies.enable_webhooks = value.get("enableWebhooks")
logger.debug("Chat apps access settings fetched.")
request = service.policies().list_next(request, response)
except Exception as error:
self._handle_api_error(
error,
"fetching Chat policies",
self.provider.identity.customer_id,
)
fetch_succeeded = False
break
self.policies_fetched = fetch_succeeded
logger.info("Chat policies fetched successfully.")
except Exception as error:
self._handle_api_error(
error,
"fetching Chat policies",
self.provider.identity.customer_id,
)
self.policies_fetched = False
class ChatPolicies(BaseModel):
"""Model for domain-level Chat policy settings."""
# chat.chat_file_sharing
external_file_sharing: Optional[str] = None
internal_file_sharing: Optional[str] = None
# chat.external_chat_restriction
allow_external_chat: Optional[bool] = None
external_chat_restriction: Optional[str] = None
# chat.chat_external_spaces
external_spaces_enabled: Optional[bool] = None
external_spaces_domain_allowlist_mode: Optional[str] = None
# chat.chat_apps_access
enable_apps: Optional[bool] = None
enable_webhooks: Optional[bool] = None
+208 -3
View File
@@ -150,7 +150,212 @@ AWS_SECURITY_TOKEN = 'testing'
AWS_SESSION_TOKEN = 'testing'
[tool.uv]
# cartography (pulled in via the API) still pins okta<1.0.0 for its (unused-by-prowler)
# intel.okta integration; the SDK Okta provider needs okta==3.4.2 (PR #11079). Force the
# version prowler needs; cartography's okta module is not imported here.
# Transitive pins matching the current lock to prevent silent drift on `uv lock`
# (e.g. supply chain hijacks via newer releases). Bump deliberately.
constraint-dependencies = [
"about-time==4.2.1",
"aenum==3.1.17",
"aiofiles==24.1.0",
"aiohappyeyeballs==2.6.1",
"aiohttp==3.13.5",
"aiosignal==1.4.0",
"alibabacloud-actiontrail20200706==2.4.1",
"alibabacloud-credentials==1.0.3",
"alibabacloud-credentials-api==1.0.0",
"alibabacloud-cs20151215==6.1.0",
"alibabacloud-darabonba-array==0.1.0",
"alibabacloud-darabonba-encode-util==0.0.2",
"alibabacloud-darabonba-map==0.0.1",
"alibabacloud-darabonba-signature-util==0.0.4",
"alibabacloud-darabonba-string==0.0.4",
"alibabacloud-darabonba-time==0.0.1",
"alibabacloud-ecs20140526==7.2.5",
"alibabacloud-endpoint-util==0.0.4",
"alibabacloud-gateway-oss==0.0.17",
"alibabacloud-gateway-sls==0.4.2",
"alibabacloud-gateway-sls-util==0.4.1",
"alibabacloud-gateway-spi==0.0.3",
"alibabacloud-openapi-util==0.2.4",
"alibabacloud-oss-util==0.0.6",
"alibabacloud-oss20190517==1.0.6",
"alibabacloud-ram20150501==1.2.0",
"alibabacloud-sas20181203==6.1.0",
"alibabacloud-sts20150401==1.1.6",
"alibabacloud-tea==0.4.3",
"alibabacloud-tea-openapi==0.4.4",
"alibabacloud-tea-util==0.3.14",
"alibabacloud-tea-xml==0.0.3",
"alibabacloud-vpc20160428==6.13.0",
"aliyun-log-fastpb==0.3.0",
"annotated-types==0.7.0",
"antlr4-python3-runtime==4.13.2",
"anyio==4.13.0",
"apscheduler==3.11.2",
"astroid==3.3.11",
"async-timeout==5.0.1",
"attrs==26.1.0",
"aws-sam-translator==1.109.0",
"aws-xray-sdk==2.15.0",
"azure-common==1.1.28",
"azure-core==1.41.0",
"azure-mgmt-core==1.6.0",
"bandit==1.8.3",
"black==25.1.0",
"blinker==1.9.0",
"certifi==2026.4.22",
"cffi==2.0.0",
"cfn-lint==1.51.0",
"charset-normalizer==3.4.7",
"circuitbreaker==2.1.3",
"click==8.3.3",
"click-plugins==1.1.1.2",
"contextlib2==21.6.0",
"coverage==7.6.12",
"darabonba-core==1.0.5",
"decorator==5.2.1",
"dill==0.4.1",
"distro==1.9.0",
"dnspython==2.8.0",
"docker==7.1.0",
"dogpile-cache==1.5.0",
"durationpy==0.10",
"email-validator==2.2.0",
"exceptiongroup==1.3.1",
"execnet==2.1.2",
"filelock==3.20.3",
"flake8==7.1.2",
"flask==3.1.3",
"freezegun==1.5.1",
"frozenlist==1.8.0",
"google-api-core==2.30.3",
"google-auth==2.52.0",
"googleapis-common-protos==1.75.0",
"graphemeu==0.7.2",
"graphql-core==3.2.8",
"h11==0.16.0",
"hpack==4.1.0",
"httpcore==1.0.9",
"httplib2==0.31.2",
"httpx==0.28.1",
"hyperframe==6.1.0",
"iamdata==0.1.202605131",
"idna==3.15",
"importlib-metadata==8.7.1",
"iniconfig==2.3.0",
"iso8601==2.1.0",
"isodate==0.7.2",
"isort==6.1.0",
"itsdangerous==2.2.0",
"jinja2==3.1.6",
"jmespath==1.1.0",
"joserfc==1.6.5",
"jsonpatch==1.33",
"jsonpath-ng==1.8.0",
"jsonpointer==3.1.1",
"jsonschema-path==0.3.4",
"jsonschema-specifications==2025.9.1",
"jwcrypto==1.5.7",
"keystoneauth1==5.14.0",
"lazy-object-proxy==1.12.0",
"lz4==4.4.5",
"markdown-it-py==4.2.0",
"markupsafe==3.0.3",
"mccabe==0.7.0",
"mdurl==0.1.2",
"microsoft-kiota-authentication-azure==1.9.2",
"microsoft-kiota-http==1.9.2",
"microsoft-kiota-serialization-form==1.9.2",
"microsoft-kiota-serialization-json==1.9.2",
"microsoft-kiota-serialization-multipart==1.9.2",
"microsoft-kiota-serialization-text==1.9.2",
"mock==5.2.0",
"moto==5.1.11",
"mpmath==1.3.0",
"msal==1.36.0",
"msal-extensions==1.3.1",
"msgraph-core==1.3.8",
"msrest==0.7.1",
"multidict==6.7.1",
"multipart==1.3.1",
"mypy-extensions==1.1.0",
"narwhals==2.21.0",
"nest-asyncio==1.6.0",
"networkx==3.4.2",
"oauthlib==3.3.1",
"openapi-schema-validator==0.6.3",
"openapi-spec-validator==0.7.1",
"opentelemetry-api==1.41.1",
"opentelemetry-sdk==1.41.1",
"opentelemetry-semantic-conventions==0.62b1",
"os-service-types==1.8.2",
"packaging==26.2",
"pathable==0.4.4",
"pathspec==1.1.1",
"pbr==7.0.3",
"platformdirs==4.9.6",
"plotly==6.7.0",
"pluggy==1.6.0",
"prek==0.3.9",
"propcache==0.5.2",
"proto-plus==1.28.0",
"protobuf==7.34.1",
"psutil==7.2.2",
"py-partiql-parser==0.6.1",
"pyasn1==0.6.3",
"pyasn1-modules==0.4.2",
"pycodestyle==2.12.1",
"pycparser==3.0",
"pycryptodomex==3.23.0",
"pydantic-core==2.41.5",
"pydash==8.0.6",
"pyflakes==3.2.0",
"pygments==2.20.0",
"pyjwt==2.12.1",
"pylint==3.3.4",
"pynacl==1.6.2",
"pyopenssl==26.2.0",
"pyparsing==3.3.2",
"pytest==8.3.5",
"pytest-cov==6.0.0",
"pytest-env==1.1.5",
"pytest-randomly==3.16.0",
"pytest-xdist==3.6.1",
"pywin32==311",
"pyyaml==6.0.3",
"referencing==0.36.2",
"regex==2026.5.9",
"requests==2.34.0",
"requests-file==3.0.1",
"requests-oauthlib==2.0.0",
"requestsexceptions==1.4.0",
"responses==0.26.0",
"retrying==1.4.2",
"rfc3339-validator==0.1.4",
"rich==15.0.0",
"rpds-py==0.30.0",
"s3transfer==0.14.0",
"setuptools==82.0.1",
"six==1.17.0",
"sniffio==1.3.1",
"std-uritemplate==2.0.8",
"stevedore==5.7.0",
"sympy==1.14.0",
"tldextract==5.3.1",
"tomli==2.4.1",
"tomlkit==0.15.0",
"typing-extensions==4.15.0",
"typing-inspection==0.4.2",
"tzdata==2026.2",
"uritemplate==4.2.0",
"urllib3==2.7.0",
"vulture==2.14",
"websocket-client==1.9.0",
"werkzeug==3.1.8",
"wrapt==2.1.2",
"xlsxwriter==3.2.9",
"xmltodict==1.0.4",
"yarl==1.23.0",
"zipp==3.23.1",
"zstd==1.5.7.3"
]
override-dependencies = ["okta==3.4.2"]
@@ -0,0 +1,119 @@
from unittest.mock import patch
from prowler.providers.googleworkspace.services.chat.chat_service import ChatPolicies
from tests.providers.googleworkspace.googleworkspace_fixtures import (
CUSTOMER_ID,
set_mocked_googleworkspace_provider,
)
class TestChatAppsInstallationDisabled:
def test_pass(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_apps_installation_disabled.chat_apps_installation_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_apps_installation_disabled.chat_apps_installation_disabled import (
chat_apps_installation_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(enable_apps=False)
check = chat_apps_installation_disabled()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "PASS"
assert "disabled" in findings[0].status_extended
assert findings[0].resource_name == "Chat Policies"
assert findings[0].resource_id == "chatPolicies"
assert findings[0].customer_id == CUSTOMER_ID
assert findings[0].resource == ChatPolicies(enable_apps=False).dict()
def test_fail_enabled(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_apps_installation_disabled.chat_apps_installation_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_apps_installation_disabled.chat_apps_installation_disabled import (
chat_apps_installation_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(enable_apps=True)
check = chat_apps_installation_disabled()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "FAIL"
assert "enabled" in findings[0].status_extended
def test_pass_no_policy_set(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_apps_installation_disabled.chat_apps_installation_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_apps_installation_disabled.chat_apps_installation_disabled import (
chat_apps_installation_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(enable_apps=None)
check = chat_apps_installation_disabled()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "PASS"
assert "secure default" in findings[0].status_extended
def test_no_findings_when_fetch_failed(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_apps_installation_disabled.chat_apps_installation_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_apps_installation_disabled.chat_apps_installation_disabled import (
chat_apps_installation_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = False
mock_client.policies = ChatPolicies()
check = chat_apps_installation_disabled()
findings = check.execute()
assert len(findings) == 0
@@ -0,0 +1,149 @@
from unittest.mock import patch
from prowler.providers.googleworkspace.services.chat.chat_service import ChatPolicies
from tests.providers.googleworkspace.googleworkspace_fixtures import (
CUSTOMER_ID,
set_mocked_googleworkspace_provider,
)
class TestChatExternalFileSharingDisabled:
def test_pass(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_external_file_sharing_disabled.chat_external_file_sharing_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_external_file_sharing_disabled.chat_external_file_sharing_disabled import (
chat_external_file_sharing_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(external_file_sharing="NO_FILES")
check = chat_external_file_sharing_disabled()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "PASS"
assert "disabled" in findings[0].status_extended
assert findings[0].resource_name == "Chat Policies"
assert findings[0].resource_id == "chatPolicies"
assert findings[0].customer_id == CUSTOMER_ID
assert (
findings[0].resource
== ChatPolicies(external_file_sharing="NO_FILES").dict()
)
def test_fail_all_files(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_external_file_sharing_disabled.chat_external_file_sharing_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_external_file_sharing_disabled.chat_external_file_sharing_disabled import (
chat_external_file_sharing_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(external_file_sharing="ALL_FILES")
check = chat_external_file_sharing_disabled()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "FAIL"
assert "ALL_FILES" in findings[0].status_extended
def test_fail_images_only(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_external_file_sharing_disabled.chat_external_file_sharing_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_external_file_sharing_disabled.chat_external_file_sharing_disabled import (
chat_external_file_sharing_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(external_file_sharing="IMAGES_ONLY")
check = chat_external_file_sharing_disabled()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "FAIL"
assert "IMAGES_ONLY" in findings[0].status_extended
def test_fail_no_policy_set(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_external_file_sharing_disabled.chat_external_file_sharing_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_external_file_sharing_disabled.chat_external_file_sharing_disabled import (
chat_external_file_sharing_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(external_file_sharing=None)
check = chat_external_file_sharing_disabled()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "FAIL"
assert "not explicitly configured" in findings[0].status_extended
def test_no_findings_when_fetch_failed(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_external_file_sharing_disabled.chat_external_file_sharing_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_external_file_sharing_disabled.chat_external_file_sharing_disabled import (
chat_external_file_sharing_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = False
mock_client.policies = ChatPolicies()
check = chat_external_file_sharing_disabled()
findings = check.execute()
assert len(findings) == 0
@@ -0,0 +1,154 @@
from unittest.mock import patch
from prowler.providers.googleworkspace.services.chat.chat_service import ChatPolicies
from tests.providers.googleworkspace.googleworkspace_fixtures import (
CUSTOMER_ID,
set_mocked_googleworkspace_provider,
)
class TestChatExternalMessagingRestricted:
def test_pass_external_chat_disabled(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_external_messaging_restricted.chat_external_messaging_restricted.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_external_messaging_restricted.chat_external_messaging_restricted import (
chat_external_messaging_restricted,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(allow_external_chat=False)
check = chat_external_messaging_restricted()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "PASS"
assert "disabled" in findings[0].status_extended
assert findings[0].resource_name == "Chat Policies"
assert findings[0].resource_id == "chatPolicies"
assert findings[0].customer_id == CUSTOMER_ID
assert (
findings[0].resource == ChatPolicies(allow_external_chat=False).dict()
)
def test_pass_trusted_domains(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_external_messaging_restricted.chat_external_messaging_restricted.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_external_messaging_restricted.chat_external_messaging_restricted import (
chat_external_messaging_restricted,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(
allow_external_chat=True,
external_chat_restriction="TRUSTED_DOMAINS",
)
check = chat_external_messaging_restricted()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "PASS"
assert "restricted to allowed domains" in findings[0].status_extended
def test_fail_no_restriction(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_external_messaging_restricted.chat_external_messaging_restricted.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_external_messaging_restricted.chat_external_messaging_restricted import (
chat_external_messaging_restricted,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(
allow_external_chat=True,
external_chat_restriction="NO_RESTRICTION",
)
check = chat_external_messaging_restricted()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "FAIL"
assert "not restricted" in findings[0].status_extended
def test_pass_no_policy_set(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_external_messaging_restricted.chat_external_messaging_restricted.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_external_messaging_restricted.chat_external_messaging_restricted import (
chat_external_messaging_restricted,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies()
check = chat_external_messaging_restricted()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "PASS"
assert "secure default" in findings[0].status_extended
def test_no_findings_when_fetch_failed(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_external_messaging_restricted.chat_external_messaging_restricted.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_external_messaging_restricted.chat_external_messaging_restricted import (
chat_external_messaging_restricted,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = False
mock_client.policies = ChatPolicies()
check = chat_external_messaging_restricted()
findings = check.execute()
assert len(findings) == 0
@@ -0,0 +1,155 @@
from unittest.mock import patch
from prowler.providers.googleworkspace.services.chat.chat_service import ChatPolicies
from tests.providers.googleworkspace.googleworkspace_fixtures import (
CUSTOMER_ID,
set_mocked_googleworkspace_provider,
)
class TestChatExternalSpacesRestricted:
def test_pass_spaces_disabled(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_external_spaces_restricted.chat_external_spaces_restricted.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_external_spaces_restricted.chat_external_spaces_restricted import (
chat_external_spaces_restricted,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(external_spaces_enabled=False)
check = chat_external_spaces_restricted()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "PASS"
assert "disabled" in findings[0].status_extended
assert findings[0].resource_name == "Chat Policies"
assert findings[0].resource_id == "chatPolicies"
assert findings[0].customer_id == CUSTOMER_ID
assert (
findings[0].resource
== ChatPolicies(external_spaces_enabled=False).dict()
)
def test_pass_trusted_domains(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_external_spaces_restricted.chat_external_spaces_restricted.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_external_spaces_restricted.chat_external_spaces_restricted import (
chat_external_spaces_restricted,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(
external_spaces_enabled=True,
external_spaces_domain_allowlist_mode="TRUSTED_DOMAINS",
)
check = chat_external_spaces_restricted()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "PASS"
assert "restricted to allowed domains" in findings[0].status_extended
def test_fail_all_domains(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_external_spaces_restricted.chat_external_spaces_restricted.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_external_spaces_restricted.chat_external_spaces_restricted import (
chat_external_spaces_restricted,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(
external_spaces_enabled=True,
external_spaces_domain_allowlist_mode="ALL_DOMAINS",
)
check = chat_external_spaces_restricted()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "FAIL"
assert "not restricted" in findings[0].status_extended
def test_fail_no_policy_set(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_external_spaces_restricted.chat_external_spaces_restricted.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_external_spaces_restricted.chat_external_spaces_restricted import (
chat_external_spaces_restricted,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies()
check = chat_external_spaces_restricted()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "FAIL"
assert "not explicitly configured" in findings[0].status_extended
def test_no_findings_when_fetch_failed(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_external_spaces_restricted.chat_external_spaces_restricted.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_external_spaces_restricted.chat_external_spaces_restricted import (
chat_external_spaces_restricted,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = False
mock_client.policies = ChatPolicies()
check = chat_external_spaces_restricted()
findings = check.execute()
assert len(findings) == 0
@@ -0,0 +1,119 @@
from unittest.mock import patch
from prowler.providers.googleworkspace.services.chat.chat_service import ChatPolicies
from tests.providers.googleworkspace.googleworkspace_fixtures import (
CUSTOMER_ID,
set_mocked_googleworkspace_provider,
)
class TestChatIncomingWebhooksDisabled:
def test_pass(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_incoming_webhooks_disabled.chat_incoming_webhooks_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_incoming_webhooks_disabled.chat_incoming_webhooks_disabled import (
chat_incoming_webhooks_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(enable_webhooks=False)
check = chat_incoming_webhooks_disabled()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "PASS"
assert "disabled" in findings[0].status_extended
assert findings[0].resource_name == "Chat Policies"
assert findings[0].resource_id == "chatPolicies"
assert findings[0].customer_id == CUSTOMER_ID
assert findings[0].resource == ChatPolicies(enable_webhooks=False).dict()
def test_fail_enabled(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_incoming_webhooks_disabled.chat_incoming_webhooks_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_incoming_webhooks_disabled.chat_incoming_webhooks_disabled import (
chat_incoming_webhooks_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(enable_webhooks=True)
check = chat_incoming_webhooks_disabled()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "FAIL"
assert "enabled" in findings[0].status_extended
def test_pass_no_policy_set(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_incoming_webhooks_disabled.chat_incoming_webhooks_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_incoming_webhooks_disabled.chat_incoming_webhooks_disabled import (
chat_incoming_webhooks_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(enable_webhooks=None)
check = chat_incoming_webhooks_disabled()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "PASS"
assert "secure default" in findings[0].status_extended
def test_no_findings_when_fetch_failed(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_incoming_webhooks_disabled.chat_incoming_webhooks_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_incoming_webhooks_disabled.chat_incoming_webhooks_disabled import (
chat_incoming_webhooks_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = False
mock_client.policies = ChatPolicies()
check = chat_incoming_webhooks_disabled()
findings = check.execute()
assert len(findings) == 0
@@ -0,0 +1,122 @@
from unittest.mock import patch
from prowler.providers.googleworkspace.services.chat.chat_service import ChatPolicies
from tests.providers.googleworkspace.googleworkspace_fixtures import (
CUSTOMER_ID,
set_mocked_googleworkspace_provider,
)
class TestChatInternalFileSharingDisabled:
def test_pass(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_internal_file_sharing_disabled.chat_internal_file_sharing_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_internal_file_sharing_disabled.chat_internal_file_sharing_disabled import (
chat_internal_file_sharing_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(internal_file_sharing="NO_FILES")
check = chat_internal_file_sharing_disabled()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "PASS"
assert "disabled" in findings[0].status_extended
assert findings[0].resource_name == "Chat Policies"
assert findings[0].resource_id == "chatPolicies"
assert findings[0].customer_id == CUSTOMER_ID
assert (
findings[0].resource
== ChatPolicies(internal_file_sharing="NO_FILES").dict()
)
def test_fail_all_files(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_internal_file_sharing_disabled.chat_internal_file_sharing_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_internal_file_sharing_disabled.chat_internal_file_sharing_disabled import (
chat_internal_file_sharing_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(internal_file_sharing="ALL_FILES")
check = chat_internal_file_sharing_disabled()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "FAIL"
assert "ALL_FILES" in findings[0].status_extended
def test_fail_no_policy_set(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_internal_file_sharing_disabled.chat_internal_file_sharing_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_internal_file_sharing_disabled.chat_internal_file_sharing_disabled import (
chat_internal_file_sharing_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = True
mock_client.policies = ChatPolicies(internal_file_sharing=None)
check = chat_internal_file_sharing_disabled()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "FAIL"
assert "not explicitly configured" in findings[0].status_extended
def test_no_findings_when_fetch_failed(self):
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_internal_file_sharing_disabled.chat_internal_file_sharing_disabled.chat_client"
) as mock_client,
):
from prowler.providers.googleworkspace.services.chat.chat_internal_file_sharing_disabled.chat_internal_file_sharing_disabled import (
chat_internal_file_sharing_disabled,
)
mock_client.provider = mock_provider
mock_client.policies_fetched = False
mock_client.policies = ChatPolicies()
check = chat_internal_file_sharing_disabled()
findings = check.execute()
assert len(findings) == 0
@@ -0,0 +1,440 @@
from unittest.mock import MagicMock, patch
from googleapiclient.errors import HttpError
from httplib2 import Response as HttpResponse
from tests.providers.googleworkspace.googleworkspace_fixtures import (
ROOT_ORG_UNIT_ID,
set_mocked_googleworkspace_provider,
)
class TestChatService:
def test_chat_fetch_policies_all_settings(self):
"""Test fetching all 4 Chat policy settings from Cloud Identity API"""
mock_provider = set_mocked_googleworkspace_provider()
mock_provider.audit_config = {}
mock_provider.fixer_config = {}
mock_credentials = MagicMock()
mock_session = MagicMock()
mock_session.credentials = mock_credentials
mock_provider.session = mock_session
mock_service = MagicMock()
mock_policies_list = MagicMock()
mock_policies_list.execute.return_value = {
"policies": [
{
"setting": {
"type": "settings/chat.chat_file_sharing",
"value": {
"externalFileSharing": "NO_FILES",
"internalFileSharing": "IMAGES_ONLY",
},
}
},
{
"setting": {
"type": "settings/chat.external_chat_restriction",
"value": {
"allowExternalChat": True,
"externalChatRestriction": "TRUSTED_DOMAINS",
},
}
},
{
"setting": {
"type": "settings/chat.chat_external_spaces",
"value": {
"enabled": True,
"domainAllowlistMode": "TRUSTED_DOMAINS",
},
}
},
{
"setting": {
"type": "settings/chat.chat_apps_access",
"value": {
"enableApps": False,
"enableWebhooks": False,
},
}
},
]
}
mock_service.policies().list.return_value = mock_policies_list
mock_service.policies().list_next.return_value = None
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_service.GoogleWorkspaceService._build_service",
return_value=mock_service,
),
):
from prowler.providers.googleworkspace.services.chat.chat_service import (
Chat,
)
chat = Chat(mock_provider)
assert chat.policies_fetched is True
assert chat.policies.external_file_sharing == "NO_FILES"
assert chat.policies.internal_file_sharing == "IMAGES_ONLY"
assert chat.policies.allow_external_chat is True
assert chat.policies.external_chat_restriction == "TRUSTED_DOMAINS"
assert chat.policies.external_spaces_enabled is True
assert (
chat.policies.external_spaces_domain_allowlist_mode == "TRUSTED_DOMAINS"
)
assert chat.policies.enable_apps is False
assert chat.policies.enable_webhooks is False
def test_chat_fetch_policies_empty_response(self):
"""Test handling empty policies response"""
mock_provider = set_mocked_googleworkspace_provider()
mock_provider.audit_config = {}
mock_provider.fixer_config = {}
mock_session = MagicMock()
mock_session.credentials = MagicMock()
mock_provider.session = mock_session
mock_service = MagicMock()
mock_policies_list = MagicMock()
mock_policies_list.execute.return_value = {"policies": []}
mock_service.policies().list.return_value = mock_policies_list
mock_service.policies().list_next.return_value = None
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_service.GoogleWorkspaceService._build_service",
return_value=mock_service,
),
):
from prowler.providers.googleworkspace.services.chat.chat_service import (
Chat,
)
chat = Chat(mock_provider)
assert chat.policies_fetched is True
assert chat.policies.external_file_sharing is None
assert chat.policies.allow_external_chat is None
assert chat.policies.enable_apps is None
assert chat.policies.enable_webhooks is None
def test_chat_fetch_policies_api_error(self):
"""Test handling of API errors during policy fetch"""
mock_provider = set_mocked_googleworkspace_provider()
mock_provider.audit_config = {}
mock_provider.fixer_config = {}
mock_session = MagicMock()
mock_session.credentials = MagicMock()
mock_provider.session = mock_session
mock_service = MagicMock()
mock_service.policies().list.side_effect = Exception("API Error")
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_service.GoogleWorkspaceService._build_service",
return_value=mock_service,
),
):
from prowler.providers.googleworkspace.services.chat.chat_service import (
Chat,
)
chat = Chat(mock_provider)
assert chat.policies_fetched is False
assert chat.policies.external_file_sharing is None
def test_chat_fetch_policies_build_service_returns_none(self):
"""Test early return when _build_service fails to construct the client"""
mock_provider = set_mocked_googleworkspace_provider()
mock_provider.audit_config = {}
mock_provider.fixer_config = {}
mock_session = MagicMock()
mock_session.credentials = MagicMock()
mock_provider.session = mock_session
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_service.GoogleWorkspaceService._build_service",
return_value=None,
),
):
from prowler.providers.googleworkspace.services.chat.chat_service import (
Chat,
)
chat = Chat(mock_provider)
assert chat.policies_fetched is False
assert chat.policies.external_file_sharing is None
def test_chat_fetch_policies_execute_raises(self):
"""Test inner except handler when request.execute() raises during pagination"""
mock_provider = set_mocked_googleworkspace_provider()
mock_provider.audit_config = {}
mock_provider.fixer_config = {}
mock_session = MagicMock()
mock_session.credentials = MagicMock()
mock_provider.session = mock_session
mock_service = MagicMock()
mock_request = MagicMock()
mock_request.execute.side_effect = Exception("Execute failed")
mock_service.policies().list.return_value = mock_request
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_service.GoogleWorkspaceService._build_service",
return_value=mock_service,
),
):
from prowler.providers.googleworkspace.services.chat.chat_service import (
Chat,
)
chat = Chat(mock_provider)
assert chat.policies_fetched is False
assert chat.policies.external_file_sharing is None
def test_chat_fetch_policies_ignores_ou_and_group_level(self):
"""Test that OU-level and group-level policies are skipped, only customer-level used"""
mock_provider = set_mocked_googleworkspace_provider()
mock_provider.audit_config = {}
mock_provider.fixer_config = {}
mock_session = MagicMock()
mock_session.credentials = MagicMock()
mock_provider.session = mock_session
mock_service = MagicMock()
mock_policies_list = MagicMock()
mock_policies_list.execute.return_value = {
"policies": [
{
# Customer-level: no policyQuery → should be used
"setting": {
"type": "settings/chat.chat_apps_access",
"value": {"enableApps": False, "enableWebhooks": False},
}
},
{
# OU-level: has policyQuery.orgUnit → should be skipped
"policyQuery": {"orgUnit": "orgUnits/sales_team"},
"setting": {
"type": "settings/chat.chat_apps_access",
"value": {"enableApps": True, "enableWebhooks": True},
},
},
{
# Group-level: has policyQuery.group → should be skipped
"policyQuery": {"group": "groups/contractors"},
"setting": {
"type": "settings/chat.chat_file_sharing",
"value": {
"externalFileSharing": "ALL_FILES",
"internalFileSharing": "ALL_FILES",
},
},
},
{
# Customer-level: no policyQuery → should be used
"setting": {
"type": "settings/chat.chat_file_sharing",
"value": {
"externalFileSharing": "NO_FILES",
"internalFileSharing": "NO_FILES",
},
}
},
]
}
mock_service.policies().list.return_value = mock_policies_list
mock_service.policies().list_next.return_value = None
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_service.GoogleWorkspaceService._build_service",
return_value=mock_service,
),
):
from prowler.providers.googleworkspace.services.chat.chat_service import (
Chat,
)
chat = Chat(mock_provider)
assert chat.policies_fetched is True
assert chat.policies.enable_apps is False
assert chat.policies.external_file_sharing == "NO_FILES"
def test_chat_fetch_policies_accepts_root_ou(self):
"""Test that root-OU-scoped policies are accepted as customer-level"""
mock_provider = set_mocked_googleworkspace_provider()
mock_provider.audit_config = {}
mock_provider.fixer_config = {}
mock_session = MagicMock()
mock_session.credentials = MagicMock()
mock_provider.session = mock_session
mock_service = MagicMock()
mock_policies_list = MagicMock()
mock_policies_list.execute.return_value = {
"policies": [
{
# Root OU: matches provider's root_org_unit_id → should be accepted
"policyQuery": {"orgUnit": f"orgUnits/{ROOT_ORG_UNIT_ID}"},
"setting": {
"type": "settings/chat.chat_apps_access",
"value": {"enableApps": False, "enableWebhooks": True},
},
},
{
# Sub-OU: different orgUnit → should be skipped
"policyQuery": {"orgUnit": "orgUnits/sub_ou_sales"},
"setting": {
"type": "settings/chat.chat_file_sharing",
"value": {
"externalFileSharing": "ALL_FILES",
"internalFileSharing": "ALL_FILES",
},
},
},
]
}
mock_service.policies().list.return_value = mock_policies_list
mock_service.policies().list_next.return_value = None
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_service.GoogleWorkspaceService._build_service",
return_value=mock_service,
),
):
from prowler.providers.googleworkspace.services.chat.chat_service import (
Chat,
)
chat = Chat(mock_provider)
assert chat.policies_fetched is True
# Root OU policy accepted
assert chat.policies.enable_apps is False
assert chat.policies.enable_webhooks is True
# Sub-OU policy skipped
assert chat.policies.external_file_sharing is None
def test_chat_partial_fetch_marks_policies_fetched_false(self):
"""Regression: if page 1 returns valid data but page 2 raises an error,
policies_fetched must be False even though some policy values were stored."""
mock_provider = set_mocked_googleworkspace_provider()
mock_provider.audit_config = {}
mock_provider.fixer_config = {}
mock_session = MagicMock()
mock_session.credentials = MagicMock()
mock_provider.session = mock_session
mock_service = MagicMock()
# Page 1: returns valid Chat data
page1_response = {
"policies": [
{
"setting": {
"type": "settings/chat.chat_apps_access",
"value": {"enableApps": False, "enableWebhooks": False},
}
},
]
}
# Page 2 request raises HttpError 429
page1_request = MagicMock()
page1_request.execute.return_value = page1_response
page2_request = MagicMock()
page2_request.execute.side_effect = HttpError(
HttpResponse({"status": "429"}), b"Rate limit exceeded"
)
mock_service.policies().list.return_value = page1_request
mock_service.policies().list_next.return_value = page2_request
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.chat.chat_service.GoogleWorkspaceService._build_service",
return_value=mock_service,
),
):
from prowler.providers.googleworkspace.services.chat.chat_service import (
Chat,
)
chat = Chat(mock_provider)
# Page 1 data was stored
assert chat.policies.enable_apps is False
# But policies_fetched must be False because page 2 failed
assert chat.policies_fetched is False
def test_chat_policies_model(self):
"""Test ChatPolicies Pydantic model"""
from prowler.providers.googleworkspace.services.chat.chat_service import (
ChatPolicies,
)
policies = ChatPolicies(
external_file_sharing="NO_FILES",
internal_file_sharing="IMAGES_ONLY",
allow_external_chat=True,
external_chat_restriction="TRUSTED_DOMAINS",
external_spaces_enabled=True,
external_spaces_domain_allowlist_mode="TRUSTED_DOMAINS",
enable_apps=False,
enable_webhooks=False,
)
assert policies.external_file_sharing == "NO_FILES"
assert policies.internal_file_sharing == "IMAGES_ONLY"
assert policies.allow_external_chat is True
assert policies.external_chat_restriction == "TRUSTED_DOMAINS"
assert policies.external_spaces_enabled is True
assert policies.external_spaces_domain_allowlist_mode == "TRUSTED_DOMAINS"
assert policies.enable_apps is False
assert policies.enable_webhooks is False
Generated
+207 -18
View File
@@ -8,6 +8,212 @@ resolution-markers = [
]
[manifest]
constraints = [
{ name = "about-time", specifier = "==4.2.1" },
{ name = "aenum", specifier = "==3.1.17" },
{ name = "aiofiles", specifier = "==24.1.0" },
{ name = "aiohappyeyeballs", specifier = "==2.6.1" },
{ name = "aiohttp", specifier = "==3.13.5" },
{ name = "aiosignal", specifier = "==1.4.0" },
{ name = "alibabacloud-actiontrail20200706", specifier = "==2.4.1" },
{ name = "alibabacloud-credentials", specifier = "==1.0.3" },
{ name = "alibabacloud-credentials-api", specifier = "==1.0.0" },
{ name = "alibabacloud-cs20151215", specifier = "==6.1.0" },
{ name = "alibabacloud-darabonba-array", specifier = "==0.1.0" },
{ name = "alibabacloud-darabonba-encode-util", specifier = "==0.0.2" },
{ name = "alibabacloud-darabonba-map", specifier = "==0.0.1" },
{ name = "alibabacloud-darabonba-signature-util", specifier = "==0.0.4" },
{ name = "alibabacloud-darabonba-string", specifier = "==0.0.4" },
{ name = "alibabacloud-darabonba-time", specifier = "==0.0.1" },
{ name = "alibabacloud-ecs20140526", specifier = "==7.2.5" },
{ name = "alibabacloud-endpoint-util", specifier = "==0.0.4" },
{ name = "alibabacloud-gateway-oss", specifier = "==0.0.17" },
{ name = "alibabacloud-gateway-sls", specifier = "==0.4.2" },
{ name = "alibabacloud-gateway-sls-util", specifier = "==0.4.1" },
{ name = "alibabacloud-gateway-spi", specifier = "==0.0.3" },
{ name = "alibabacloud-openapi-util", specifier = "==0.2.4" },
{ name = "alibabacloud-oss-util", specifier = "==0.0.6" },
{ name = "alibabacloud-oss20190517", specifier = "==1.0.6" },
{ name = "alibabacloud-ram20150501", specifier = "==1.2.0" },
{ name = "alibabacloud-sas20181203", specifier = "==6.1.0" },
{ name = "alibabacloud-sts20150401", specifier = "==1.1.6" },
{ name = "alibabacloud-tea", specifier = "==0.4.3" },
{ name = "alibabacloud-tea-openapi", specifier = "==0.4.4" },
{ name = "alibabacloud-tea-util", specifier = "==0.3.14" },
{ name = "alibabacloud-tea-xml", specifier = "==0.0.3" },
{ name = "alibabacloud-vpc20160428", specifier = "==6.13.0" },
{ name = "aliyun-log-fastpb", specifier = "==0.3.0" },
{ name = "annotated-types", specifier = "==0.7.0" },
{ name = "antlr4-python3-runtime", specifier = "==4.13.2" },
{ name = "anyio", specifier = "==4.13.0" },
{ name = "apscheduler", specifier = "==3.11.2" },
{ name = "astroid", specifier = "==3.3.11" },
{ name = "async-timeout", specifier = "==5.0.1" },
{ name = "attrs", specifier = "==26.1.0" },
{ name = "aws-sam-translator", specifier = "==1.109.0" },
{ name = "aws-xray-sdk", specifier = "==2.15.0" },
{ name = "azure-common", specifier = "==1.1.28" },
{ name = "azure-core", specifier = "==1.41.0" },
{ name = "azure-mgmt-core", specifier = "==1.6.0" },
{ name = "bandit", specifier = "==1.8.3" },
{ name = "black", specifier = "==25.1.0" },
{ name = "blinker", specifier = "==1.9.0" },
{ name = "certifi", specifier = "==2026.4.22" },
{ name = "cffi", specifier = "==2.0.0" },
{ name = "cfn-lint", specifier = "==1.51.0" },
{ name = "charset-normalizer", specifier = "==3.4.7" },
{ name = "circuitbreaker", specifier = "==2.1.3" },
{ name = "click", specifier = "==8.3.3" },
{ name = "click-plugins", specifier = "==1.1.1.2" },
{ name = "contextlib2", specifier = "==21.6.0" },
{ name = "coverage", specifier = "==7.6.12" },
{ name = "darabonba-core", specifier = "==1.0.5" },
{ name = "decorator", specifier = "==5.2.1" },
{ name = "dill", specifier = "==0.4.1" },
{ name = "distro", specifier = "==1.9.0" },
{ name = "dnspython", specifier = "==2.8.0" },
{ name = "docker", specifier = "==7.1.0" },
{ name = "dogpile-cache", specifier = "==1.5.0" },
{ name = "durationpy", specifier = "==0.10" },
{ name = "email-validator", specifier = "==2.2.0" },
{ name = "exceptiongroup", specifier = "==1.3.1" },
{ name = "execnet", specifier = "==2.1.2" },
{ name = "filelock", specifier = "==3.20.3" },
{ name = "flake8", specifier = "==7.1.2" },
{ name = "flask", specifier = "==3.1.3" },
{ name = "freezegun", specifier = "==1.5.1" },
{ name = "frozenlist", specifier = "==1.8.0" },
{ name = "google-api-core", specifier = "==2.30.3" },
{ name = "google-auth", specifier = "==2.52.0" },
{ name = "googleapis-common-protos", specifier = "==1.75.0" },
{ name = "graphemeu", specifier = "==0.7.2" },
{ name = "graphql-core", specifier = "==3.2.8" },
{ name = "h11", specifier = "==0.16.0" },
{ name = "hpack", specifier = "==4.1.0" },
{ name = "httpcore", specifier = "==1.0.9" },
{ name = "httplib2", specifier = "==0.31.2" },
{ name = "httpx", specifier = "==0.28.1" },
{ name = "hyperframe", specifier = "==6.1.0" },
{ name = "iamdata", specifier = "==0.1.202605131" },
{ name = "idna", specifier = "==3.15" },
{ name = "importlib-metadata", specifier = "==8.7.1" },
{ name = "iniconfig", specifier = "==2.3.0" },
{ name = "iso8601", specifier = "==2.1.0" },
{ name = "isodate", specifier = "==0.7.2" },
{ name = "isort", specifier = "==6.1.0" },
{ name = "itsdangerous", specifier = "==2.2.0" },
{ name = "jinja2", specifier = "==3.1.6" },
{ name = "jmespath", specifier = "==1.1.0" },
{ name = "joserfc", specifier = "==1.6.5" },
{ name = "jsonpatch", specifier = "==1.33" },
{ name = "jsonpath-ng", specifier = "==1.8.0" },
{ name = "jsonpointer", specifier = "==3.1.1" },
{ name = "jsonschema-path", specifier = "==0.3.4" },
{ name = "jsonschema-specifications", specifier = "==2025.9.1" },
{ name = "jwcrypto", specifier = "==1.5.7" },
{ name = "keystoneauth1", specifier = "==5.14.0" },
{ name = "lazy-object-proxy", specifier = "==1.12.0" },
{ name = "lz4", specifier = "==4.4.5" },
{ name = "markdown-it-py", specifier = "==4.2.0" },
{ name = "markupsafe", specifier = "==3.0.3" },
{ name = "mccabe", specifier = "==0.7.0" },
{ name = "mdurl", specifier = "==0.1.2" },
{ name = "microsoft-kiota-authentication-azure", specifier = "==1.9.2" },
{ name = "microsoft-kiota-http", specifier = "==1.9.2" },
{ name = "microsoft-kiota-serialization-form", specifier = "==1.9.2" },
{ name = "microsoft-kiota-serialization-json", specifier = "==1.9.2" },
{ name = "microsoft-kiota-serialization-multipart", specifier = "==1.9.2" },
{ name = "microsoft-kiota-serialization-text", specifier = "==1.9.2" },
{ name = "mock", specifier = "==5.2.0" },
{ name = "moto", specifier = "==5.1.11" },
{ name = "mpmath", specifier = "==1.3.0" },
{ name = "msal", specifier = "==1.36.0" },
{ name = "msal-extensions", specifier = "==1.3.1" },
{ name = "msgraph-core", specifier = "==1.3.8" },
{ name = "msrest", specifier = "==0.7.1" },
{ name = "multidict", specifier = "==6.7.1" },
{ name = "multipart", specifier = "==1.3.1" },
{ name = "mypy-extensions", specifier = "==1.1.0" },
{ name = "narwhals", specifier = "==2.21.0" },
{ name = "nest-asyncio", specifier = "==1.6.0" },
{ name = "networkx", specifier = "==3.4.2" },
{ name = "oauthlib", specifier = "==3.3.1" },
{ name = "openapi-schema-validator", specifier = "==0.6.3" },
{ name = "openapi-spec-validator", specifier = "==0.7.1" },
{ name = "opentelemetry-api", specifier = "==1.41.1" },
{ name = "opentelemetry-sdk", specifier = "==1.41.1" },
{ name = "opentelemetry-semantic-conventions", specifier = "==0.62b1" },
{ name = "os-service-types", specifier = "==1.8.2" },
{ name = "packaging", specifier = "==26.2" },
{ name = "pathable", specifier = "==0.4.4" },
{ name = "pathspec", specifier = "==1.1.1" },
{ name = "pbr", specifier = "==7.0.3" },
{ name = "platformdirs", specifier = "==4.9.6" },
{ name = "plotly", specifier = "==6.7.0" },
{ name = "pluggy", specifier = "==1.6.0" },
{ name = "prek", specifier = "==0.3.9" },
{ name = "propcache", specifier = "==0.5.2" },
{ name = "proto-plus", specifier = "==1.28.0" },
{ name = "protobuf", specifier = "==7.34.1" },
{ name = "psutil", specifier = "==7.2.2" },
{ name = "py-partiql-parser", specifier = "==0.6.1" },
{ name = "pyasn1", specifier = "==0.6.3" },
{ name = "pyasn1-modules", specifier = "==0.4.2" },
{ name = "pycodestyle", specifier = "==2.12.1" },
{ name = "pycparser", specifier = "==3.0" },
{ name = "pycryptodomex", specifier = "==3.23.0" },
{ name = "pydantic-core", specifier = "==2.41.5" },
{ name = "pydash", specifier = "==8.0.6" },
{ name = "pyflakes", specifier = "==3.2.0" },
{ name = "pygments", specifier = "==2.20.0" },
{ name = "pyjwt", specifier = "==2.12.1" },
{ name = "pylint", specifier = "==3.3.4" },
{ name = "pynacl", specifier = "==1.6.2" },
{ name = "pyopenssl", specifier = "==26.2.0" },
{ name = "pyparsing", specifier = "==3.3.2" },
{ name = "pytest", specifier = "==8.3.5" },
{ name = "pytest-cov", specifier = "==6.0.0" },
{ name = "pytest-env", specifier = "==1.1.5" },
{ name = "pytest-randomly", specifier = "==3.16.0" },
{ name = "pytest-xdist", specifier = "==3.6.1" },
{ name = "pywin32", specifier = "==311" },
{ name = "pyyaml", specifier = "==6.0.3" },
{ name = "referencing", specifier = "==0.36.2" },
{ name = "regex", specifier = "==2026.5.9" },
{ name = "requests", specifier = "==2.34.0" },
{ name = "requests-file", specifier = "==3.0.1" },
{ name = "requests-oauthlib", specifier = "==2.0.0" },
{ name = "requestsexceptions", specifier = "==1.4.0" },
{ name = "responses", specifier = "==0.26.0" },
{ name = "retrying", specifier = "==1.4.2" },
{ name = "rfc3339-validator", specifier = "==0.1.4" },
{ name = "rich", specifier = "==15.0.0" },
{ name = "rpds-py", specifier = "==0.30.0" },
{ name = "s3transfer", specifier = "==0.14.0" },
{ name = "setuptools", specifier = "==82.0.1" },
{ name = "six", specifier = "==1.17.0" },
{ name = "sniffio", specifier = "==1.3.1" },
{ name = "std-uritemplate", specifier = "==2.0.8" },
{ name = "stevedore", specifier = "==5.7.0" },
{ name = "sympy", specifier = "==1.14.0" },
{ name = "tldextract", specifier = "==5.3.1" },
{ name = "tomli", specifier = "==2.4.1" },
{ name = "tomlkit", specifier = "==0.15.0" },
{ name = "typing-extensions", specifier = "==4.15.0" },
{ name = "typing-inspection", specifier = "==0.4.2" },
{ name = "tzdata", specifier = "==2026.2" },
{ name = "uritemplate", specifier = "==4.2.0" },
{ name = "urllib3", specifier = "==2.7.0" },
{ name = "vulture", specifier = "==2.14" },
{ name = "websocket-client", specifier = "==1.9.0" },
{ name = "werkzeug", specifier = "==3.1.8" },
{ name = "wrapt", specifier = "==2.1.2" },
{ name = "xlsxwriter", specifier = "==3.2.9" },
{ name = "xmltodict", specifier = "==1.0.4" },
{ name = "yarl", specifier = "==1.23.0" },
{ name = "zipp", specifier = "==3.23.1" },
{ name = "zstd", specifier = "==1.5.7.3" },
]
overrides = [{ name = "okta", specifier = "==3.4.2" }]
[[package]]
@@ -1208,8 +1414,7 @@ source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aws-sam-translator" },
{ name = "jsonpatch" },
{ name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "networkx" },
{ name = "pyyaml" },
{ name = "regex" },
{ name = "sympy" },
@@ -2619,27 +2824,11 @@ wheels = [
name = "networkx"
version = "3.4.2"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.11'",
]
sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" },
]
[[package]]
name = "networkx"
version = "3.6.1"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.12'",
"python_full_version == '3.11.*'",
]
sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" },
]
[[package]]
name = "numpy"
version = "2.0.2"