SaaS

Secure Notion with cnspec

Scan a Notion workspace for publicly shared pages and databases, integration ownership, and stale content with cnspec.

A Notion workspace ends up holding runbooks, architecture notes, incident write-ups, and customer lists, and a page published to the web looks exactly like a private one from the inside. Nothing in the editor tells you that the page you're reading is also reachable by anyone with the link. cnspec reads a Notion workspace through an internal integration and reports which pages and databases are published to the web, who and what can reach them, how the integration itself is owned, and which content nobody has touched in a long time.

If you're new to cnspec, start with the Quickstart. For an overview of every SaaS service cnspec can scan, see the SaaS scanning overview.

The Notion resources are experimental. Field names and behavior can change in a future cnspec release.

Prerequisites

To scan Notion with cnspec, you must have:

cnspec only issues read requests to the Notion API. It never changes a workspace.

What an internal integration can and can't see

Notion's REST API is scoped to content, not to workspace administration, and that shapes what an audit can cover.

Sharing is per page. An integration sees only what has been explicitly connected to it. A page nobody connected is invisible to cnspec, which means an empty result is not the same as a clean workspace. Connect the integration at the top of each page tree you want covered, and child pages are included.

Workspace governance needs the Enterprise APIs. Enforced single sign-on, allowed email domains, member provisioning, and the audit log are reported by Notion's separate Enterprise SCIM and Audit Log APIs, not by an internal integration token. Those settings aren't readable here, so don't treat a passing Notion scan as covering them.

Integration capabilities are set in Notion, not reported by it. The read, insert, update, and comment capabilities granted to an integration are configured on its settings page, and Notion's public API doesn't report them. Review them there.

Authenticate

cnspec authenticates as the integration, not as a person, using an internal integration token. To create one, open Notion's integrations page, choose New integration, pick the workspace, and copy the internal integration secret. Then open each page or database you want to audit, and use Connections to add the integration.

Grant the read user information including email addresses capability if you want notion.user.email populated. Without it, that field is null for every user.

cnspec shell notion --token secret_YOUR_TOKEN

Environment variables

NOTION_TOKEN supplies the same value as the flag:

export NOTION_TOKEN=secret_YOUR_TOKEN

When this is set, you can omit --token from the commands below.

Connection options

OptionDescription
--tokenNotion internal integration token

Verify with a quick Notion check

Confirm that cnspec can reach your workspace by opening a cnspec shell:

cnspec shell notion --token secret_YOUR_TOKEN
cnspec> notion.bot { id name ownerType workspaceName }
notion.bot: {
  id: "a1b2c3d4-5e6f-7890-abcd-ef1234567890"
  name: "Lunalectric Security Audit"
  ownerType: "workspace"
  workspaceName: "Lunalectric"
}

If cnspec connects and reports your integration, you're ready to scan.

Scan Notion

cnspec scan notion --token secret_YOUR_TOKEN

When a scan completes, cnspec prints a summary of all the checks it ran, grouped by policy, along with a risk score from 0 (no risk) to 100 (highest risk). Failed checks include remediation guidance to help you fix issues. To learn more about reading scan results, read Understand cnspec Results.

Mondoo doesn't yet ship an out-of-the-box Notion policy, so use the checks below as a starting point and create your own policies to meet your specific requirements.

Explore and test checks interactively

Open a cnspec shell to discover resources and try out checks:

cnspec shell notion --token secret_YOUR_TOKEN

Review the integration's own identity

ownerType is the security signal on this resource. An integration owned by the workspace survives the departure of whoever created it. One owned by a user is tied to that person's account and typically loses its access when they leave, which turns an audit into a silent no-op:

cnspec> notion.bot { id name ownerType owner { name email } workspaceName }

Size up what the integration can actually see

The counts report the scope of your audit, not the size of the workspace. A page nobody connected to the integration is in neither number:

cnspec> notion.workspace { name userCount databaseCount pageCount }

Find pages published to the web

publicUrl is set only when a page has been published through Notion Sites, so it's the direct answer to "what can someone without a Notion account read":

cnspec> notion.pages.where(isPubliclyShared) { title url publicUrl lastEditedTime }

Find databases published to the web

A published database exposes every row it holds, so it carries more than a single page does:

cnspec> notion.databases.where(isPubliclyShared) { title url publicUrl lastEditedTime }

List workspace members and bots

type separates human members from bot users belonging to other integrations installed in the same workspace:

cnspec> notion.users { id name type email }

Find the other integrations installed in the workspace

Each bot-type user is another integration holding its own access to workspace content:

cnspec> notion.users.where(type == "bot") { name botOwnerType botOwner { name email } }

Review content nobody has touched in a year

Stale content is where a decommissioned system's credentials and a former customer's details outlive their usefulness:

cnspec> notion.pages.where(lastEditedTime < time.now - 365*time.day) { title url lastEditedTime }

Walk from a page to its container

A page can be a row of a database, nested under another page, or sit directly under the workspace, so the parent references resolve only when the API reports one:

cnspec> notion.pages { title parentDatabase { title } parentPage { title } }

Read a page's raw properties

properties carries the page's property values as Notion returns them, keyed by property name, so a property with no field of its own is still reachable:

cnspec> notion.pages.where(title == "Incident Log") { title properties }

Example security checks

Ensure no page is published to the web

Run this where nothing in the connected tree is meant to be public:

cnspec> notion.pages.none(isPubliclyShared)
[ok] value: true

Ensure no database is published to the web

cnspec> notion.databases.none(isPubliclyShared)
[ok] value: true

Ensure the integration is owned by the workspace

A user-owned integration loses its access when that person leaves, and the audit it powers stops reporting without failing:

cnspec> notion.bot.ownerType == "workspace"
[ok] value: true

Ensure no archived page is still published

Archiving a page doesn't unpublish it, so an archived page with a publicUrl is content the workspace considers retired and the web still serves:

cnspec> notion.pages.none(archived && isPubliclyShared)
[ok] value: true

Ensure the integration can see the content you expect

An empty result and a clean result look identical, so assert that the audit's scope is non-empty before trusting anything built on it:

cnspec> notion.workspace.pageCount > 0
[ok] value: true

Review every bot with access to the workspace

Hold the count of installed integrations under review, since each one is a standing credential against your content:

cnspec> notion.users.where(type == "bot").length <= 5
[ok] value: true

Learn more

On this page