From 2b4acaa4f136945ba618c758e5d7ff575e39445a Mon Sep 17 00:00:00 2001 From: Maxime Lamothe-Brassard Date: Fri, 25 Sep 2026 16:58:05 +0000 Subject: [PATCH] Cloud Security: CIS GCP v5.0 and CIS Microsoft 365 v7.0 frameworks, Microsoft 365 certificate mode - compliance: fifteen frameworks (adds cis-gcp-v5 and cis-m365-v7), the versioned CIS frameworks side by side, and how licence-gated Microsoft 365 controls report - entra: certificate mode (generated certificate, setup script, rotation), the full permission list, the SharePoint advanced settings opt-in, licence-gated controls, and every Test Provider check - api-reference: POST /providers/m365/certificate Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/cloud-security/api-reference.md | 5 +- docs/cloud-security/compliance.md | 65 +++- docs/cloud-security/provider-setup/entra.md | 347 ++++++++++++++++---- mkdocs.yml | 2 +- 4 files changed, 351 insertions(+), 68 deletions(-) diff --git a/docs/cloud-security/api-reference.md b/docs/cloud-security/api-reference.md index a91dd1440..ad2a318b9 100644 --- a/docs/cloud-security/api-reference.md +++ b/docs/cloud-security/api-reference.md @@ -79,8 +79,8 @@ Shared behaviors: !!! note "`ciem/*` and `providers/*` families" `ciem/*` has four members — `public-access`, `facets`, `identities`, and - `identity` (singular: one principal). `providers/*` has two — `test` - (`POST`, below) and `manifest` (`GET`, above). Creating, editing, and + `identity` (singular: one principal). `providers/*` has three members. `test` + and `m365/certificate` are `POST` routes, below, and `manifest` is a `GET`, above. Creating, editing, and deleting provider *records* is not a `/cloudsec` route: it goes through the `cloudsec_provider` Hive. @@ -109,6 +109,7 @@ All writes are `POST` with a JSON body and require `cloudsec.set`. | `/code/scan`, `/code/autofix`, `/code/ingest`, `/code/pr_check` | Code Security writes: rescan a repository, open an AutoFix pull request, push scan results, check a pull request. See [Code Security API routes](code-security/reference.md#api-routes). | Rescan, AutoFix and pull-request checks are queued and answer `accepted`; the work runs afterwards. Ingest answers with what was recorded and `notes`. | | `/code/webhook` | `{connection, url, secret}` — point a GitHub connection's App webhook at this organization's `github-code-webhook-` adapter. `url` must be this organization's own hook URL, and the App must already have an active webhook (`webhook_not_active` otherwise). See [the webhook API](code-security/pull-requests.md#the-webhook-api) for the rules and refusal reasons. | The connection's re-checked webhook status: `{state, reason, missing_events, detail}`. | | `/providers/test` | `{provider: }` — credential inline (ephemeral, never stored) or a `hive://secret/` reference. | `{supported, report: {provider, ok, checks: [{id, name, required, ok, detail}]}}`. A provider type with no preflight implemented answers `{supported: false, report: null}` rather than an error — treat it as "cannot verify", not "credential bad". | +| `/providers/m365/certificate` | `{connection, client_id?, replace?}`. Generates the certificate a Microsoft Entra / Microsoft 365 connection authenticates with (see [certificate mode](provider-setup/entra.md#certificate-mode-recommended)). Also requires `secret.set`: the key pair is written to the organization's secret store as `cloudsec-m365-`. A repeat call returns the existing certificate unless `replace` is `true`. | `{created, secret_name, credentials, common_name, certificate, certificate_pem, thumbprint, thumbprint_sha256, not_before, not_after}`. `certificate` is the base64 DER `.cer` to upload; the private key is never returned. | Example — disposition a finding: diff --git a/docs/cloud-security/compliance.md b/docs/cloud-security/compliance.md index 4f3ff20e0..3aa4a933a 100644 --- a/docs/cloud-security/compliance.md +++ b/docs/cloud-security/compliance.md @@ -15,25 +15,72 @@ limacharlie cloudsec compliance report --framework cis-gcp limacharlie cloudsec compliance frameworks ``` -Thirteen frameworks ship today — `cis-aws`, `cis-azure`, `cis-gcp` (the -default), `cis-m365`, `soc2`, `pci-dss`, `hipaa`, `iso-27001`, `nist-csf`, -`nist-ai-rmf`, `owasp-llm`, `owasp-top10`, and `cis-supply-chain`. `nist-ai-rmf` +Fifteen frameworks ship today: `cis-aws`, `cis-azure`, `cis-gcp` (the +default), `cis-gcp-v5`, `cis-m365`, `cis-m365-v7`, `soc2`, `pci-dss`, `hipaa`, +`iso-27001`, `nist-csf`, `nist-ai-rmf`, `owasp-llm`, `owasp-top10`, and +`cis-supply-chain`. `nist-ai-rmf` and `owasp-llm` are AI frameworks: they assess the OpenAI and Anthropic estate connected through the [AI providers](providers.md#ai-security-aispm). `owasp-top10` (OWASP Top 10:2021, mapped by CWE) and `cis-supply-chain` (the CIS Software Supply Chain Security Guide's *Source Code* and *Dependencies* sections) are graded off [Code Security](code-security/results.md#compliance) and apply only when a -GitHub organization is connected. `cis-m365` is graded off the -Microsoft Entra directory, so it covers the benchmark's Entra chapter and reports -NOT_ASSESSED for the admin centers that are not collected (Defender, Purview, -Exchange, SharePoint, Teams) — read each control's description for what it -assesses and why. It applies to a tenant connected as an `entra` provider, or as -the Entra half of an `azure` one. The set +GitHub organization is connected. The set grows over time, so `limacharlie cloudsec compliance frameworks` (`GET /compliance/frameworks`) — which carries each framework's `id`, `name`, `version`, and control counts — is the source of truth for valid `--framework` values. +### CIS benchmark versions + +Two CIS benchmarks ship in two versions, side by side. Each version is its own +framework id, so an assignment or report on the older id keeps working +unchanged, and you move to the newer one when you are ready: + +| Framework | Benchmark | Controls | +|---|---|---| +| `cis-gcp` | CIS Google Cloud Platform Foundation Benchmark (v2.0 subset) | 21 | +| `cis-gcp-v5` | CIS Google Cloud Platform Foundation Benchmark v5.0.0 | 93: 76 automated, 8 partly automated, 9 manual | +| `cis-m365` | CIS Microsoft 365 Foundations Benchmark (v4.0 subset) | 26 | +| `cis-m365-v7` | CIS Microsoft 365 Foundations Benchmark v7.0.0 | 160: 143 automated, 17 manual | + +The newer versions carry the full benchmark. Each control carries its CIS +recommendation number (`external_id`, for example `5.1.2.3`) with a title and a +description of what LimaCharlie checks, written by LimaCharlie. The benchmark +text itself is published by CIS at +[cisecurity.org](https://www.cisecurity.org/cis-benchmarks). + +- **`cis-gcp-v5`** reads project and organization configuration (IAM, logging and + alerting, networking, compute, Cloud SQL, storage, BigQuery). Several of these + reads need optional roles or APIs on the service account; without them, the + controls they feed report NOT_ASSESSED and name the missing read. See + [Google Cloud provider setup](provider-setup/gcp.md). A partly automated control + can FAIL from what LimaCharlie reads, but a PASS also needs your attestation, + so without one it reports NOT_ASSESSED. +- **`cis-m365-v7`** covers Entra ID, Exchange Online, Defender for Office 365, + Purview, SharePoint and OneDrive, Teams, Intune, Microsoft Forms and + Power BI / Fabric. It applies to a tenant connected as an + [`entra` provider](provider-setup/entra.md). Its Microsoft Graph reads work with + either credential type. Exchange Online, Defender for Office 365, Purview and + the SharePoint advanced settings need the connection's + [certificate mode](provider-setup/entra.md#authentication-modes). The Entra half of + an `azure` connection reads the Microsoft Graph settings only; Exchange Online, + Purview, Teams and Power BI / Fabric are read through an `entra` connection. +- **`cis-m365`** (v4.0) is graded off the Microsoft Entra directory. It covers the + benchmark's Entra chapter and reports NOT_ASSESSED for the admin centers it does + not collect (Defender, Purview, Exchange, SharePoint, Teams). It applies to a + tenant connected as an `entra` provider, or as the Entra half of an `azure` one. + +!!! note "Licence-gated Microsoft 365 controls" + 37 `cis-m365-v7` controls grade a feature that exists only with a specific + Microsoft licence: Entra ID P1 or P2, Entra ID Governance, Intune, Defender for + Office 365 Plan 1 or 2, Safe Documents, Customer Lockbox, or Purview + Communications DLP. LimaCharlie reads the tenant's licences. When a tenant does + not hold the licence a control needs, that control reports **NOT_ASSESSED** and + its reason names the licence, for example `no licence was observed for the + feature this control grades: Microsoft Entra ID P1`. It never reports PASS + for a feature the tenant cannot turn on. It does not report FAIL either, + because the fix is a purchase, not a setting. + The report is per-control, and each control lands in one of four states: - **PASS** — no open finding proves a violation of the control. diff --git a/docs/cloud-security/provider-setup/entra.md b/docs/cloud-security/provider-setup/entra.md index 1c2705e69..c327fd53c 100644 --- a/docs/cloud-security/provider-setup/entra.md +++ b/docs/cloud-security/provider-setup/entra.md @@ -1,67 +1,277 @@ -# Microsoft Entra ID +# Microsoft Entra ID / Microsoft 365 !!! tip "Connecting from the web app?" - Follow the prerequisites and credential creation instructions below, then - return to **Cloud Security → Settings → Providers → Add provider**. Enter - the provider IDs under **Configuration** and save the credential using - **New secret** under **Permissions**. Run **Test Provider**, fix required - failures, and save. The LimaCharlie CLI examples below are an alternative. + Open **Cloud Security → Settings → Providers → Add provider → Entra ID**. + In certificate mode (the default) the wizard generates the certificate and a + PowerShell setup script that does the tenant side for you. See + [Certificate mode](#certificate-mode-recommended). In client-secret mode, follow + the [client secret](#client-secret-mode) steps below, save the credential with + **New secret** under **Permissions**, run **Test Provider**, and save. [First-time setup and verification](../getting-started.md) explains the full journey. -A **directory-only** connection for organizations that use Entra ID or -Microsoft 365 but have no Azure infrastructure to enumerate. It collects the +A connection for organizations that use Entra ID or Microsoft 365. It collects the tenant-global identity surface over Microsoft Graph: users, groups and membership, service principals and app registrations (with their long-lived credentials), directory roles and PIM eligibility, Conditional Access policies, -administrative units, and the tenant's **federated domains** — the external +administrative units, and the tenant's **federated domains**, which are the external identity providers (ADFS and other SAML/WS-Fed trusts) that can assert identities into the tenant. -**Auth model:** an **Entra ID app registration** (service principal) with a -**client secret** and **Microsoft Graph application permissions**. There is no -ARM/subscription setup at all. +It also reads the Microsoft 365 tenant settings that the +[`cis-m365-v7`](../compliance.md#cis-benchmark-versions) framework grades: Entra ID +policies, Intune, the Microsoft 365 admin center, SharePoint and OneDrive, +Exchange Online, Defender for Office 365, Purview, Teams and Power BI / Fabric. +Each of these needs its own grant. A setting LimaCharlie could not read makes the +controls that depend on it report NOT_ASSESSED with a reason; it never reports +them as PASS. + +There is no ARM or subscription setup. !!! tip "Already connecting Azure?" The [Azure](azure.md) provider collects this same directory as part of its sweep. You only need a standalone Entra record when there is no Azure - subscription to connect — or when you want the directory collected - independently of the infrastructure connection. Holding **both** for one - tenant is supported and safe: the Azure connection detects the standalone - record and defers its tenant-global directory collectors to it, so the - directory is never collected twice. + subscription to connect, when you want the directory collected independently + of the infrastructure connection, or when you want the Microsoft 365 settings + beyond Microsoft Graph (Exchange Online, Purview, Teams, Power BI / Fabric), + which only an Entra connection reads. Holding **both** for one tenant is + supported and safe: the Azure connection detects the standalone record and + defers its tenant-global directory collectors to it, so the directory is never + collected twice. + +## Authentication modes + +The connection authenticates as an **Entra ID app registration** (service +principal) in your tenant, with **application** permissions. It supports two +credential types: + +| | Certificate (recommended) | Client secret | +|---|---|---| +| Who creates the credential | LimaCharlie generates a key pair and a certificate for the connection. The private key stays in your organization's LimaCharlie secret store; you upload only the public certificate. | You create a client secret on the app registration. | +| Renewal | Automatic. LimaCharlie rotates the certificate itself. | You re-create the secret before it expires. | +| Microsoft Graph reads (Entra ID, Conditional Access, Intune, Microsoft 365 admin center, SharePoint sharing settings) | Yes | Yes | +| Exchange Online, Defender for Office 365, Purview | Yes | No: those controls report NOT_ASSESSED | +| SharePoint advanced settings ([opt-in](#sharepoint-advanced-settings-opt-in)) | Yes | No: Microsoft accepts only certificate credentials for them | + +Existing client-secret connections keep working unchanged. To move one to +certificate mode, edit it in the web app, switch **Authentication** to the +certificate, and follow the steps below. ## Prerequisites -- Permission to create an app registration (**Application Developer** or - higher). -- Permission to **grant tenant-wide admin consent** (**Privileged Role - Administrator** or **Global Administrator**). - Your **tenant ID** (Entra ID → Overview). +- A tenant administrator who can grant tenant-wide admin consent and assign + directory roles: **Global Administrator**, or **Privileged Role Administrator** + together with **Cloud Application Administrator**. +- In LimaCharlie, generating the certificate requires the `cloudsec.set` **and** + `secret.set` permissions, because it writes the key pair to the organization's + secret store. +- Certificate mode's setup script runs in **PowerShell 7** with the Microsoft + Graph PowerShell SDK. The script installs the modules it needs for the current + user if they are missing. + +## Permissions + +Every grant except `Directory.Read.All` is optional. Each optional grant feeds +specific collectors or `cis-m365-v7` controls. Without it, those controls report +NOT_ASSESSED and name what is missing; nothing else stops working. The setup +script grants all of them (the SharePoint one only when you +[opt in](#sharepoint-advanced-settings-opt-in)). + +### Microsoft Graph application permissions + +| Permission | Required | What it reads | +|---|:--:|---| +| **Directory.Read.All** | ✅ | Users, groups, service principals and role assignments: the directory inventory. | +| **AuditLog.Read.All** | — | Sign-in activity and the MFA registration report. Without it, activity reads as unobserved and the MFA-registration control is not assessed. Sign-in activity needs Entra ID P1 or P2. | +| **Policy.Read.All** | — | Conditional Access and tenant policies (Security Defaults, consent, guest access, authentication methods, per-user MFA). | +| **Application.Read.All** | — | App registrations and service principals, their owners and credentials. | +| **RoleManagement.Read.Directory** | — | Privileged role assignments and PIM eligibility. PIM needs Entra ID P2 or Entra ID Governance. | +| **AdministrativeUnit.Read.All** | — | Administrative units. | +| **Domain.Read.All** | — | Domains: federation and password expiry settings. | +| **OnPremDirectorySynchronization.Read.All** | — | On-premises directory synchronization settings (hybrid tenants). | +| **MailboxSettings.Read** | — | Inbox rules that forward mail outside the organization. | +| **Organization.Read.All** | — | The tenant's licences, branding and organization settings; Teams also requires it. Without it, licence-dependent controls are not assessed. | +| **Policy.Read.DeviceConfiguration** | — | The device registration policy. | +| **AccessReview.Read.All** | — | Access review definitions (guest and privileged-role reviews). | +| **RoleManagementPolicy.Read.Directory** | — | PIM role settings (activation approval, duration). | +| **DeviceManagementConfiguration.Read.All** | — | Intune device compliance settings. | +| **DeviceManagementServiceConfig.Read.All** | — | Intune enrollment restrictions. | +| **OrgSettings-AppsAndServices.Read.All** | — | Microsoft 365 admin center settings for apps and services. | +| **OrgSettings-Forms.Read.All** | — | Microsoft Forms settings (phishing protection, external sharing). | +| **SharePointTenantSettings.Read.All** | — | SharePoint and OneDrive sharing settings, through Microsoft Graph. | + +### Grants outside Microsoft Graph + +| Grant | Certificate mode only | What it reads | +|---|:--:|---| +| **Exchange.ManageAsApp** on *Office 365 Exchange Online* (application) | ✅ | Exchange Online and Defender for Office 365 settings (mail flow, auditing, anti-phishing, Safe Links). Also needs Global Reader. | +| **Exchange.ManageAsApp** on *Microsoft Exchange Online Protection* (application) | ✅ | Microsoft Purview policies (data loss prevention, sensitivity labels, alert policies). Also needs Global Reader. | +| **Global Reader** (Microsoft Entra directory role, assigned to the app) | — | A read-only role. Exchange Online, Purview and Teams accept an app only through a directory role. | +| Fabric admin setting **Service principals can access read-only admin APIs**, enabled for a security group that contains the app | — | Power BI / Fabric tenant settings. | +| **Sites.FullControl.All** on *Office 365 SharePoint Online* (application), **opt-in** | ✅ | SharePoint advanced settings. See [below](#sharepoint-advanced-settings-opt-in). | -## Required permissions +!!! danger "Application permissions, not delegated" + Graph permissions must be **Application** permissions. Delegated + permissions need a signed-in user and leave `graph_directory` failing even + after consent. -| Grant | Type | Why | Preflight check | -|---|---|---|---| -| **Directory.Read.All** | Microsoft Graph — **Application** | Users, groups, service principals, app registrations, domains (including federated-domain / external-IdP posture), directory roles and their active assignments | `graph_directory` | +!!! warning "Do not give the app a Power BI Service API permission" + Fabric refuses read-only admin API calls from a service principal that also + holds a Power BI Service API permission. The security-group setting above is + all the Power BI / Fabric read needs. + +### SharePoint advanced settings (opt-in) + +Some SharePoint and OneDrive tenant settings (OneDrive sharing, default link +types, guest access expiry) are readable only through SharePoint's admin API. +Microsoft offers no read-only application permission for that API. Reading them +requires **Sites.FullControl.All**, which also allows writing to every site. +LimaCharlie only reads with it, but the grant itself is broad, so it is **off by +default**: + +- **Not granted:** the 8 `cis-m365-v7` SharePoint and OneDrive controls that + depend on these settings report NOT_ASSESSED, with a reason saying the + SharePoint admin API was not opted in. The sharing settings Microsoft Graph + exposes are still read with `SharePointTenantSettings.Read.All`. +- **Granted:** tick **Also read SharePoint advanced settings** in the wizard + before you download the setup script, or grant the permission yourself. It + works in certificate mode only. + +The grant is the opt-in. LimaCharlie has no separate switch for it. To opt out +later, remove the permission from the app registration. + +### Licence-gated controls + +Some `cis-m365-v7` controls grade a feature that exists only with a specific +Microsoft licence (Entra ID P1 or P2, Entra ID Governance, Intune, Defender for +Office 365, Safe Documents, Customer Lockbox, Purview Communications DLP). +LimaCharlie reads the tenant's licences with `Organization.Read.All`. When the +tenant does not hold the licence a control needs, the control reports +NOT_ASSESSED with a reason naming it, for example `no licence was observed for +the feature this control grades: Microsoft Entra ID P1`. Buying the licence and +configuring the feature makes the control graded on the next sweep. See +[Compliance](../compliance.md#cis-benchmark-versions). + +## Certificate mode (recommended) + +### 1. Generate the certificate + +In the provider wizard, set **Authentication** to **Certificate generated by +LimaCharlie** and select **Generate certificate**. LimaCharlie creates a key pair +and a self-signed certificate valid for 12 months, and stores them in the +organization's secret store as `cloudsec-m365-`. The connection's +**Credentials** field then points at `hive://secret/cloudsec-m365-`. +You receive only the public certificate (a `.cer` download) and its thumbprint. + +Generating again returns the same certificate. **Replace certificate** discards +the key pair and creates a new one. The connection then stops collecting until +you upload the new certificate, so use it only when the current one expired or was +removed from the app registration. + +!!! warning "Tenants that only accept certificates from a trusted CA" + If your tenant has an application management policy that only accepts + certificates issued by a trusted certificate authority, Microsoft refuses + this certificate because it is self-signed. Exempt the LimaCharlie app from + that policy, or use client-secret mode. + +### 2. Run the setup script + +The wizard builds a PowerShell 7 script for this connection, with the certificate +and the permission lists above filled in. Choose: + +- **App registration name**: the script reuses an app with this name, or creates + it. To use an app you already have, enter its application (client) ID instead. +- **Create a security group for Power BI / Fabric** (and its name), if you want + the Power BI controls graded. +- **Also read SharePoint advanced settings**, if you + [opt in](#sharepoint-advanced-settings-opt-in). + +Download the script and run it signed in as a Global Administrator of the tenant +(or a Privileged Role Administrator who is also a Cloud Application +Administrator). It connects with `Connect-MgGraph`, then: + +1. finds or creates the app registration and its service principal; +2. uploads the public certificate to the app, next to any credential it already + has; +3. grants the Microsoft Graph application permissions and the two + `Exchange.ManageAsApp` permissions, and admin-consents them (plus + `Sites.FullControl.All` when you opted in); +4. assigns the **Global Reader** role to the app; +5. if you asked for it, creates or reuses the security group, adds the app to it, + and prints the Fabric step below. + +Every step checks before it creates, and the script removes nothing, so you can +run it again safely. A permission whose resource is not provisioned in the tenant +yet (for example, no Exchange Online licence) is skipped with a warning. + +**Power BI / Fabric, one manual step.** A Fabric administrator opens the Fabric +admin portal → **Tenant settings** → **Admin API settings**, enables **Service +principals can access read-only admin APIs**, applies it to specific security +groups, and adds the group the script created. Without it, the Power BI +controls report NOT_ASSESSED. + +### 3. Enter the application (client) ID + +The script prints the **Application (client) ID** at the end; it is also on the +app registration's Overview page. Enter it in the wizard, run **Test Provider**, +and save. Permissions can take up to an hour to reach Exchange Online, so an +Exchange or Purview check that fails right after the script usually passes later. + +### Certificate rotation + +LimaCharlie rotates the certificate automatically at 75% of its lifetime. It adds +the new certificate to the app registration through Microsoft Graph, using the +current certificate to prove possession, and then removes the old one. You do not +re-run anything. The provider page shows the certificate's expiry and thumbprint, +and any rotation problem reported by the last sweep. + +If a certificate does expire (for example, because it was removed from the app +registration and rotation could not recover), every Microsoft 365 control reports +NOT_ASSESSED. Select **Replace certificate** and run the setup script again to +upload the new one. + +### Without the web app + +The certificate comes from an API route: -## Optional permissions +```bash +curl -X POST -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \ + "https://api.limacharlie.io/v1/cloudsec/$OID/providers/m365/certificate" \ + -d '{"connection": "entra-prod"}' +``` -| Grant | Unlocks | Preflight check | -|---|---|---| -| **AuditLog.Read.All** | Last-sign-in / dormancy enrichment. **Requires Entra ID P1 or P2** | `signin_activity` | -| **Policy.Read.All** | Conditional Access policy posture | *(collected during the sweep)* | -| **RoleManagement.Read.Directory** | PIM *eligibility* — who can activate a privileged role just-in-time. **Also requires Entra ID P1 or P2**: without the licence the PIM endpoints refuse the call no matter what is consented | *(collected during the sweep)* | -| **Application.Read.All** | Fuller app-registration / service-principal credential detail | *(collected during the sweep)* | -| **AdministrativeUnit.Read.All** | Administrative-unit scoping | *(collected during the sweep)* | -| **AgentIdentity.Read.All** | Source-asserted AI-agent identities in the directory | *(collected during the sweep)* | +`connection` is the name of the provider record you are about to create. The +optional `client_id` records the app registration's ID, and `"replace": true` +replaces an existing certificate. The response carries `credentials` (the +`hive://secret/...` reference), `certificate` (base64 DER, the `.cer` to upload), +`certificate_pem`, `thumbprint` (SHA-1), `not_before` and `not_after`. It never +carries the private key. Upload the certificate to your app registration +(**Certificates & secrets → Certificates → Upload certificate**), grant the +permissions above, then create the record: -!!! info "Skipping the PIM grant is safe" - Without `RoleManagement.Read.Directory` (or without the P1/P2 licence) the - directory-role graph is still collected from **active** assignments, which - ride `Directory.Read.All`. You lose just-in-time *eligibility* edges, not - the roles themselves. +```yaml +provider_type: entra +entra_tenant_id: "" +entra_client_id: "" +entra_auth_mode: certificate +entra_cert_thumbprint: "" +entra_cert_not_after: "" +credentials: hive://secret/cloudsec-m365-entra-prod +internal_domains: [example.com, example.onmicrosoft.com] +refresh: 6h +``` + +In certificate mode, `entra_client_id` is required on the record. LimaCharlie +keeps `entra_cert_thumbprint` and `entra_cert_not_after` up to date after each +rotation. + +## Client secret mode -## Create the app registration +A client secret reads Microsoft Graph only. Exchange Online, +Defender for Office 365, Purview and the SharePoint advanced settings report +NOT_ASSESSED. + +### Create the app registration === "Web console" @@ -76,8 +286,9 @@ ARM/subscription setup at all. an expiry per your organization's policy and copy the **Value** immediately, not the Secret ID. Record the expiry for future rotation. 5. Open **API permissions → Add a permission → Microsoft Graph → Application - permissions**. Add **Directory.Read.All** and the optional permissions you - need from the table above. + permissions**. Add **Directory.Read.All** and the optional + [Microsoft Graph permissions](#microsoft-graph-application-permissions) you + need. 6. Have an authorized administrator select **Grant admin consent** for the directory. Confirm consent is granted, then save the credential below. @@ -109,16 +320,15 @@ ARM/subscription setup at all. az ad app permission admin-consent --id "$APP_ID" ``` + Add the other optional Graph permissions the same way. `az ad sp show --id + 00000003-0000-0000-c000-000000000000 --query "appRoles[?value==''].id"` + prints a permission's ID. + !!! danger "`credential reset` clears existing secrets" Without `--append`, `az ad app credential reset` **removes every existing password and certificate** on the app before adding the new one. -!!! danger "Application permissions, not delegated" - Graph permissions must be **Application** permissions. Delegated - permissions need a signed-in user and leave `graph_directory` failing even - after consent. - -## Create the credentials secret +### Create the credentials secret In the LimaCharlie wizard, use **New secret** under **Permissions** to save the following JSON with your client ID and secret value. The command below is an @@ -138,7 +348,7 @@ jq -Rs '{secret: .}' entra-secret.json \ putting the credential in process arguments. The temporary file is removed only after a successful write. -## Create the provider record +### Create the provider record `provider.yaml`: @@ -154,8 +364,9 @@ refresh: 6h The client ID may be carried either on the record (`entra_client_id`) or inside the secret (`client_id`); the record wins when both are present. -In the web app: **Add provider → Entra ID**, then set **Tenant ID**, **Client -ID**, **Credentials**, and **Refresh interval**. +In the web app: **Add provider → Entra ID**, set **Authentication** to **Client +secret**, then set **Tenant ID**, **Client ID**, **Credentials**, and **Refresh +interval**. ## Verify @@ -165,15 +376,39 @@ limacharlie cloudsec provider test --input-file provider.yaml | Check | Required | Meaning if it fails | |---|:--:|---| -| `auth` | ✅ | The client ID/secret pair was rejected, or the secret expired. | -| `graph_directory` | ✅ | `Directory.Read.All` not consented — no identity inventory. | +| `auth` | ✅ | The credential was rejected: wrong client ID, a secret that expired, or a certificate not uploaded to the app registration. | +| `certificate` | ✅ (certificate mode) | The certificate expired or is not valid yet. Nothing can be read until it is replaced. It also warns when the expiry is near and rotation has not replaced it yet. | +| `client_id` | — | The secret's `client_id` differs from the record's `entra_client_id`; the record's value is used. | +| `graph_directory` | ✅ | `Directory.Read.All` not consented: no identity inventory. | +| `graph_groups` | — | Groups or their members are unreadable, so group-based role assignments resolve to nobody. | +| `graph_applications` | — | `Application.Read.All` missing: no app-registration and service-principal detail. | +| `graph_pim` | — | `RoleManagement.Read.Directory` missing, or no PIM licence: no just-in-time role eligibility. | +| `graph_admin_units` | — | `AdministrativeUnit.Read.All` missing. | +| `graph_domains` | — | `Domain.Read.All` missing: no federated-domain inventory. | +| `graph_tenant_policy` | — | `Policy.Read.All` missing: tenant policy controls and Conditional Access are not assessed. | +| `graph_onprem_sync` | — | `OnPremDirectorySynchronization.Read.All` missing (matters for hybrid tenants). | | `signin_activity` | — | Last-sign-in and dormancy enrichment unavailable (usually a missing Entra ID P1/P2 licence). | +| `m365_entra-ext` | — | Entra ID policies for `cis-m365-v7` (device registration, PIM settings, access reviews and others) are unreadable. | +| `m365_intune` | — | Intune settings are unreadable (permission or licence). | +| `m365_m365-org` | — | Microsoft 365 admin center settings are unreadable. | +| `m365_sharepoint` | — | SharePoint and OneDrive sharing settings are unreadable. | +| `m365_exo` | — | Exchange Online is unreadable: check `Exchange.ManageAsApp`, Global Reader, and certificate mode. | +| `m365_scc` | — | Purview is unreadable: check `Exchange.ManageAsApp` on Microsoft Exchange Online Protection, Global Reader, and certificate mode. | +| `m365_teams` | — | Teams policies are unreadable: check `Organization.Read.All` and Global Reader. | +| `m365_fabric` | — | Power BI / Fabric settings are unreadable: check the Fabric admin setting and the security group. | + +Each `m365_*` failure names the controls that report NOT_ASSESSED without it. +The `m365_exo`, `m365_scc`, `m365_teams` and `m365_fabric` checks run only on an +Entra connection, not on the Entra half of an Azure one. ## Troubleshooting -| `provider test` result | Cause | Fix | +| Symptom | Cause | Fix | |---|---|---| -| `auth` fails with `invalid_client` | Stored the secret **ID** instead of its **Value**, or the secret expired | Re-mint the secret and update the secret record | +| `auth` fails with `invalid_client` | Client secret: stored the secret **ID** instead of its **Value**, or the secret expired. Certificate: the certificate is not on the app registration. | Re-mint the secret and update the secret record, or run the setup script again. | +| The setup script fails at the certificate upload | The tenant only accepts certificates from a trusted CA. | Exempt the app from that application management policy, or use client-secret mode. | | `graph_directory` fails after consent | Permissions added as *Delegated*, or consent not actually granted | Add them under *Application permissions* and grant tenant-wide admin consent | +| `m365_exo` or `m365_scc` fails right after the setup script | Permissions take up to an hour to reach Exchange Online. | Test again later. | +| `m365_fabric` fails | The Fabric admin setting is off, or the group does not contain the app, or the app holds a Power BI Service API permission. | Enable the setting for the group, and remove any Power BI Service permission from the app. | | `signin_activity` fails with a licence error | Sign-in activity needs Entra ID P1/P2 | Accept the degrade, or add the licence | -| Renewal reminder | Client secrets expire; when one does, every check fails at `auth` | Re-mint before expiry and update the secret record — nothing else changes | +| Renewal reminder (client secret) | Client secrets expire; when one does, every check fails at `auth` | Re-mint before expiry and update the secret record — nothing else changes | diff --git a/mkdocs.yml b/mkdocs.yml index e71982523..32d21fee8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -633,7 +633,7 @@ nav: - Google Cloud: cloud-security/provider-setup/gcp.md - Amazon Web Services: cloud-security/provider-setup/aws.md - Microsoft Azure: cloud-security/provider-setup/azure.md - - Microsoft Entra ID: cloud-security/provider-setup/entra.md + - Microsoft Entra ID / Microsoft 365: cloud-security/provider-setup/entra.md - Okta: cloud-security/provider-setup/okta.md - Google Workspace: cloud-security/provider-setup/google-workspace.md - 1Password: cloud-security/provider-setup/onepassword.md