From 6f7f939a6273788eea4b409075423da3e28404fd Mon Sep 17 00:00:00 2001 From: Mike Swanson Date: Wed, 1 Jul 2026 09:33:09 -0700 Subject: [PATCH] sync: auto-sync from GURU-5070 at 2026-07-01 09:32:17 Author: Mike Swanson Machine: GURU-5070 Timestamp: 2026-07-01 09:32:17 --- .claude/memory/MEMORY.md | 1 + .../reference_remediation_tool_365_access.md | 32 ++++++ .claude/skills/remediation-tool/SKILL.md | 3 + .../app-permissions-and-sharepoint.md | 97 +++++++++++++++++++ .../remediation-tool/scripts/get-token.sh | 41 +++++++- errorlog.md | 2 + 6 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 .claude/memory/reference_remediation_tool_365_access.md create mode 100644 .claude/skills/remediation-tool/references/app-permissions-and-sharepoint.md diff --git a/.claude/memory/MEMORY.md b/.claude/memory/MEMORY.md index 3499981a..92d06e4e 100644 --- a/.claude/memory/MEMORY.md +++ b/.claude/memory/MEMORY.md @@ -200,3 +200,4 @@ - [GuruScan verification IN TEST / paused](project_guruscan_in_test_paused.md) — multi-engine scanner verify on DESKTOP-MS42HNC paused 2026-06-22 (VM rebooted mid-Emsisoft run); HitmanPro done (36 removed), Emsisoft full-scan unverified; resume `guruscan-agent-test.sh DESKTOP-MS42HNC scan-one Emsisoft`; Defender RTP/Tamper still off on VM - [GuruRMM fleet dispatch-hang fix](project_gururmm_dispatch_hang_fix.md) — blocking send_to on a full bounded channel to one black-holed agent wedged ALL command dispatch; fixed with try_send (9dae20c, deployed); proper black-hole eviction still missing (was reverted in 80df458) — finish it if it recurs - [Windows won't-boot / offline DISM repair playbook](windows-offline-dism-repair-gotchas.md) — Automatic Repair loop = boot-critical fault (disk/registry/wedged update), NOT shell/appx store corruption (that's a symptom); `FaultyPackageInProgress` + 100s of Install/Uninstall-Pending packages = wedged CU -> RevertPendingActions or clean install. Offline DISM rejects `wim:` source (0x800f082e) -> MOUNT the wim, source `\Windows`. Ventoy breaks WIM mount (0xc1420134) -> use Rufus. 25H2(26200)=24H2(26100)+enablement, so match 26100 media. First hit: Four Paws AvImark #32447. +- [Remediation-tool has full M365 access (incl. SharePoint)](reference_remediation_tool_365_access.md) — the app suite covers Graph/EXO/Defender/SharePoint; don't declare "no access" on an accessDenied. SharePoint app-only needs a CERT (secret = "Unsupported app only token"); use get-token.sh `sharepoint`/`sharepoint-admin` tiers + CSOM admin API (Graph /admin/sharepoint/settings scope not held). Full map: skill references/app-permissions-and-sharepoint.md. diff --git a/.claude/memory/reference_remediation_tool_365_access.md b/.claude/memory/reference_remediation_tool_365_access.md new file mode 100644 index 00000000..7a12bb5a --- /dev/null +++ b/.claude/memory/reference_remediation_tool_365_access.md @@ -0,0 +1,32 @@ +--- +name: reference_remediation_tool_365_access +description: The remediation-tool app suite has full M365 access (incl. SharePoint via cert); don't declare "no access" on an accessDenied +metadata: + type: reference +--- + +The ComputerGuru remediation-tool apps collectively have **broad, working access across ALL of +M365** — Graph, Exchange Online, Defender, AND SharePoint Online. When a call fails it's almost +always wrong-tier / wrong-endpoint / not-consented / the SharePoint cert gotcha — **not** a real +lack of access. Do NOT tell the user "the tool can't do X" without checking the live permission +map first (decode the token `roles` claim). + +Key facts: +- **SharePoint app-only requires a CERTIFICATE.** A `client_secret` token is rejected on every + SharePoint endpoint (REST `/_api` and CSOM `/_vti_bin/client.svc/ProcessQuery`) with + `"Unsupported app only token"`. The Tenant Admin app has a cert in the vault and holds + SharePoint-resource `Sites.FullControl.All`. +- `get-token.sh` now has **`sharepoint`** (content) and **`sharepoint-admin`** (tenant admin) + tiers — cert-forced, tenant resource auto-resolved from Graph `/sites/root` + (override `SP_RESOURCE_ENV`). Added 2026-07-01. +- Graph `GET /admin/sharepoint/settings` needs `SharePointTenantSettings.Read.All`, which NO app + holds → that route 403s. Read/write SharePoint tenant settings via the **CSOM admin API** + (`sharepoint-admin` tier) instead. Tenant settings live on the Tenant object + (TypeId `{268004ae-ef6b-4e9b-8425-127220d84719}`) — e.g. `SelfServiceSiteCreationDisabled`. +- Restricting employee SharePoint site creation = `SelfServiceSiteCreationDisabled=true` (CSOM) + AND restrict M365 Group creation (Entra `Group.Unified` directory setting via `user-manager`); + neither affects edit rights on existing sites. + +Full detail (live per-tier permission map + CSOM examples): +`.claude/skills/remediation-tool/references/app-permissions-and-sharepoint.md`. Surfaced by +Syncro #32492 (Birth Biologic). See also [[feedback_syncro_billing]]. diff --git a/.claude/skills/remediation-tool/SKILL.md b/.claude/skills/remediation-tool/SKILL.md index 00584224..c9d99979 100644 --- a/.claude/skills/remediation-tool/SKILL.md +++ b/.claude/skills/remediation-tool/SKILL.md @@ -20,6 +20,9 @@ Five multi-tenant apps cover distinct privilege tiers. Use only what the task re | `user-manager` | ComputerGuru User Manager | `64fac46b-8b44-41ad-93ee-7da03927576c` | `computerguru-user-manager.sops.yaml` | Graph user/group write | | `tenant-admin` | ComputerGuru Tenant Admin | `709e6eed-0711-4875-9c44-2d3518c47063` | `computerguru-tenant-admin.sops.yaml` | Graph high-privilege | | `defender` | ComputerGuru Defender Add-on | `dbf8ad1a-54f4-4bb8-8a9e-ea5b9634635b` | `computerguru-defender-addon.sops.yaml` | Defender ATP (MDE only) | +| `sharepoint` / `sharepoint-admin` | ComputerGuru Tenant Admin | `709e6eed-0711-4875-9c44-2d3518c47063` | `computerguru-tenant-admin.sops.yaml` | **SharePoint Online** (Sites.FullControl.All) — cert-only | + +**The suite has broad, working access across ALL of M365 — Graph, Exchange Online, Defender, AND SharePoint Online.** Before concluding "the tool can't do X / access denied," verify against the live permission map in `references/app-permissions-and-sharepoint.md` (decode the token `roles` claim). An `accessDenied` usually means wrong tier or wrong endpoint for a scope we DO hold — not a real gap. Two recurring traps: (1) **SharePoint app-only requires a certificate** — a `client_secret` token is rejected everywhere in SharePoint with `"Unsupported app only token"` (get-token.sh forces cert for the `sharepoint*` tiers); (2) Graph `GET /admin/sharepoint/settings` needs a scope no app holds — read/write SharePoint tenant settings via the **CSOM/REST admin API** (`sharepoint-admin` tier) instead. Full map, gotchas, and CSOM examples: `references/app-permissions-and-sharepoint.md`. **Default for breach checks:** use `investigator` (Graph) + `investigator-exo` (Exchange read). Escalate to write tiers only when remediating. diff --git a/.claude/skills/remediation-tool/references/app-permissions-and-sharepoint.md b/.claude/skills/remediation-tool/references/app-permissions-and-sharepoint.md new file mode 100644 index 00000000..b13c4efe --- /dev/null +++ b/.claude/skills/remediation-tool/references/app-permissions-and-sharepoint.md @@ -0,0 +1,97 @@ +# App permissions (authoritative) + SharePoint access + +Purpose: stop the recurring "I can't do this / access denied" friction. The ComputerGuru +app suite has **broad, working access across M365 — Graph, Exchange Online, Defender, AND +SharePoint Online**. When a call fails, the cause is almost always one of: wrong tier, wrong +endpoint for the scope we hold, a not-consented tenant (AADSTS7000229), or the SharePoint +**secret-vs-cert** gotcha below — NOT a genuine lack of access. + +**Discipline: before ever telling the user "the tool can't do X", verify against reality.** +Decode the token's `roles` claim and check it against this map: + +```bash +TOK=$(bash scripts/get-token.sh ) +python - "$TOK" <<'PY' # or: cut -d. -f2 | base64 -d | jq .roles +import sys,base64,json +p=sys.argv[1].split('.')[1]; p+='='*(-len(p)%4) +print(json.dumps(json.loads(base64.urlsafe_b64decode(p)).get('roles',[]),indent=2)) +PY +``` + +## Live-verified application-permission map (decoded 2026-07-01, Birth Biologic tenant) + +Application (app-only) roles actually present in each app's token. Fetch live to confirm for +a given tenant (consent can differ), but this is the baseline the apps are built to. + +| Tier (get-token.sh) | App | Resource | Key application roles | +|---|---|---|---| +| `investigator` | Security Investigator | Graph | Directory.Read.All, User.Read.All, AuditLog.Read.All, Application.Read.All, Organization.Read.All, Policy.Read.All, **Sites.Read.All**, Mail.Read, MailboxSettings.Read, BitlockerKey.Read.All, IdentityRiskyUser.ReadWrite.All, IdentityRiskEvent.ReadWrite.All, IdentityRiskyServicePrincipal.ReadWrite.All, IdentityRiskyAgent.ReadWrite.All, UserAuthenticationMethod.Read.All | +| `investigator-exo` | Security Investigator | Exchange Online | EXO read (Get-InboxRule, Get-Mailbox, Get-*) | +| `exchange-op` | Exchange Operator | Exchange Online | EXO write (Set-Mailbox, Remove-InboxRule, revoke sessions) | +| `user-manager` | User Manager | Graph | **Directory.ReadWrite.All, Group.ReadWrite.All, User.ReadWrite.All**, Device.ReadWrite.All, User.RevokeSessions.All, UserAuthenticationMethod.ReadWrite.All, Organization.Read.All | +| `tenant-admin` | Tenant Admin | Graph | **Application.ReadWrite.All, AppRoleAssignment.ReadWrite.All, RoleManagement.ReadWrite.Directory, Directory.ReadWrite.All, Policy.ReadWrite.ConditionalAccess, Sites.FullControl.All, Sites.ReadWrite.All**, User.ReadWrite.All, SecurityEvents.Read.All, Policy.Read.All, UserAuthenticationMethod.ReadWrite.All | +| `tenant-admin` | Tenant Admin | **SharePoint Online** | **Sites.FullControl.All** (SharePoint resource `00000003-0000-0ff1-ce00-000000000000`) — the SharePoint tiers use this | +| `defender` | Defender Add-on | Defender ATP | MDE machine/alert actions (MDE-licensed tenants only) | +| `intune-manager` | Intune Manager | Graph | DeviceManagement* (Intune) | +| `mailbox` | ACG Mailbox | Graph | ACG-INTERNAL ONLY — Mail.ReadWrite/Send, Contacts.ReadWrite | + +Notes: +- **CA policies ARE manageable** (tenant-admin holds `Policy.ReadWrite.ConditionalAccess` + the Conditional Access Administrator role) — report-only + break-glass exclusion first (see SKILL scope boundaries). +- **Directory settings** (M365 Group creation restriction, etc.) are writable via `user-manager` (Directory.ReadWrite.All) or `tenant-admin`. + +## SharePoint Online — the two gotchas + +1. **SharePoint app-only REQUIRES a certificate. A client_secret token is rejected** with + `"Unsupported app only token"` on every SharePoint endpoint (REST `/_api/...` and CSOM + `/_vti_bin/client.svc/ProcessQuery`). The Tenant Admin app has a cert in the vault + (`cert_thumbprint_b64url` + `cert_private_key_pem_b64`); `get-token.sh` forces cert for the + SharePoint tiers automatically. If you hand-roll a token, use the cert (client_assertion), + not the secret. +2. **The Graph endpoint `GET /admin/sharepoint/settings` needs `SharePointTenantSettings.Read.All`, + which NONE of the apps currently hold** — so that route returns `accessDenied`. That is NOT + "no SharePoint access". Use the SharePoint **CSOM/REST admin API** (cert token, `Sites.FullControl.All`) + instead — that path has full tenant-settings read/write. (If you'd rather use the Graph route, + add `SharePointTenantSettings.Read.All`/`.ReadWrite.All` to the Tenant Admin app manifest and + re-consent — `patch-tenant-admin-manifest.sh` + the tenant admin-consent URL.) + +## SharePoint tiers (get-token.sh) + +```bash +# Tenant admin resource ("-admin.sharepoint.com") — tenant settings, site provisioning +TOK=$(bash scripts/get-token.sh sharepoint-admin) +# Content resource (".sharepoint.com") — site/list/file operations +TOK=$(bash scripts/get-token.sh sharepoint) +# Host is auto-resolved via Graph /sites/root; override if needed: +SP_RESOURCE_ENV=contoso.sharepoint.com bash scripts/get-token.sh sharepoint-admin +``` + +### Read tenant settings (e.g. site-creation) via CSOM + +```bash +ADMIN="https://-admin.sharepoint.com" +TOK=$(bash scripts/get-token.sh sharepoint-admin) +BODY='' +curl -s -X POST "$ADMIN/_vti_bin/client.svc/ProcessQuery" \ + -H "Authorization: Bearer $TOK" -H "Content-Type: text/xml" --data-binary "$BODY" \ + | jq -r '.[] | select(type=="object") | to_entries[] + | select(.key|test("SiteCreation|SelfService|Sharing";"i")) | "\(.key) = \(.value)"' +``` + +Relevant tenant properties: `SelfServiceSiteCreationDisabled` (false = users CAN create sites), +`AllowClassicPublishingSiteCreation`, `SharingCapability`. Writing a setting is the same CSOM +surface with a `SetProperty` action (gate behind explicit YES, like any remediation write). + +### Modern site creation = M365 Group creation + +A modern team/communication site creates an M365 Group, gated by the Entra directory setting +`Group.Unified` -> `EnableGroupCreation` / `GroupCreationAllowedGroupId`. Read via Graph +`GET /groupSettings` (empty result = defaults = anyone can create). Write via `user-manager` +(Directory.ReadWrite.All): POST/PATCH `/groupSettings` using the `Group.Unified` template. +So fully restricting employee site creation = (a) `SelfServiceSiteCreationDisabled=true` (CSOM) +**and** (b) restrict `Group.Unified` group creation (Graph). Neither touches edit rights on an +existing site. + +--- +_First documented 2026-07-01 (Mike). Trigger: Syncro #32492 (Birth Biologic SharePoint site-creation +lockdown) surfaced the secret-vs-cert + no-SharePoint-tier gaps. Added `sharepoint`/`sharepoint-admin` +tiers to get-token.sh the same day._ diff --git a/.claude/skills/remediation-tool/scripts/get-token.sh b/.claude/skills/remediation-tool/scripts/get-token.sh index d0a31df8..1bdfb10c 100755 --- a/.claude/skills/remediation-tool/scripts/get-token.sh +++ b/.claude/skills/remediation-tool/scripts/get-token.sh @@ -38,6 +38,9 @@ else TENANT_ID=$("$SCRIPT_DIR/resolve-tenant.sh" "$TARGET") fi +# SharePoint tiers resolve their (tenant-specific) resource host after decrypt; default empty. +SP_KIND="" + # Map tier -> client_id, vault SOPS path, resource scope case "$TIER" in investigator) @@ -91,9 +94,23 @@ case "$TIER" in VAULT_PATH="msp-tools/computerguru-mailbox.sops.yaml" SCOPE_URL="https://graph.microsoft.com/.default" ;; + sharepoint|sharepoint-admin) + # SharePoint Online. Uses the Tenant Admin app, which holds the SharePoint-resource + # role Sites.FullControl.All. SharePoint app-only REJECTS client_secret tokens with + # "Unsupported app only token" — CERT AUTH IS MANDATORY (the cert is in the vault). + # The resource is tenant-specific, so it can't be a fixed SCOPE_URL like Graph; it is + # resolved from Graph /sites/root after decrypt. Override with SP_RESOURCE_ENV=. + # sharepoint -> content resource (".sharepoint.com") site/list/file CSOM+REST + # sharepoint-admin -> tenant admin resource ("-admin.sharepoint.com") tenant settings via CSOM ProcessQuery + CLIENT_ID="709e6eed-0711-4875-9c44-2d3518c47063" + VAULT_PATH="msp-tools/computerguru-tenant-admin.sops.yaml" + SCOPE_URL="" + SP_KIND="content"; [[ "$TIER" == "sharepoint-admin" ]] && SP_KIND="admin" + REMEDIATION_AUTH="cert" # SharePoint app-only requires a certificate + ;; *) echo "ERROR: unknown tier '$TIER'." >&2 - echo "Valid tiers: investigator | investigator-exo | exchange-op | user-manager | tenant-admin | tenant-admin-onboard | defender | intune-manager | mailbox" >&2 + echo "Valid tiers: investigator | investigator-exo | exchange-op | user-manager | tenant-admin | tenant-admin-onboard | defender | intune-manager | mailbox | sharepoint | sharepoint-admin" >&2 exit 2 ;; esac @@ -281,6 +298,28 @@ esac echo "[INFO] auth=$AUTH_METHOD" >&2 +# SharePoint tiers: resolve the tenant-specific resource host (SharePoint's resource URL is +# per-tenant, unlike Graph's fixed URL). Uses a Graph /sites/root lookup (Sites.Read via the +# Tenant Admin app), or SP_RESOURCE_ENV if the caller supplies the host explicitly. +if [[ -z "$SCOPE_URL" && -n "${SP_KIND:-}" ]]; then + SP_HOST="${SP_RESOURCE_ENV:-}" + if [[ -z "$SP_HOST" ]]; then + SP_GRAPH_TOKEN=$("$SCRIPT_DIR/get-token.sh" "$TENANT_ID" tenant-admin 2>/dev/null || true) + if [[ -n "$SP_GRAPH_TOKEN" ]]; then + SP_HOST=$(curl -s --max-time 15 "https://graph.microsoft.com/v1.0/sites/root" \ + -H "Authorization: Bearer $SP_GRAPH_TOKEN" | jq -r '.siteCollection.hostname // empty') + fi + fi + if [[ -z "$SP_HOST" ]]; then + echo "ERROR: could not resolve SharePoint host for tenant $TENANT_ID." >&2 + echo " Set SP_RESOURCE_ENV=.sharepoint.com and retry." >&2 + exit 6 + fi + [[ "$SP_KIND" == "admin" ]] && SP_HOST="${SP_HOST/.sharepoint.com/-admin.sharepoint.com}" + SCOPE_URL="https://${SP_HOST}/.default" + echo "[INFO] sharepoint resource: $SCOPE_URL" >&2 +fi + # Build request and POST. TOKEN_URL="https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token" diff --git a/errorlog.md b/errorlog.md index 69043bdc..b4bd1097 100644 --- a/errorlog.md +++ b/errorlog.md @@ -17,6 +17,8 @@ Categories (the `[type]` tag): _(none)_ = skill/command execution failure · +2026-07-01 | GURU-5070 | remediation-tool | [friction] declared 'no SharePoint access' on a Graph accessDenied; actually the Tenant Admin app holds SharePoint Sites.FullControl.All - the blocks were (a) SharePoint app-only needs CERT not client_secret ('Unsupported app only token') and (b) get-token.sh had no SharePoint resource tier. Fixed: added sharepoint/sharepoint-admin tiers + reference doc. [ctx: ref=.claude/skills/remediation-tool/references/app-permissions-and-sharepoint.md] + 2026-07-01 | GURU-5070 | context/session-resume | [correction] assumed 'PST-SERVER investigation' = Peaceful Spirit DFS rebuild; correct is the deleted-PST-files scope investigation (Dataforth/NWTOC) 2026-07-01 | GURU-5070 | sync/vault | vault sync failed: cannot connect to git.azcomputerguru.com:443 (timeout). No local vault changes pending. Internal 172.16.3.20:3000 preferred per reference_gitea_internal. [ctx: ref=reference_gitea_internal machine=GURU-5070]