For IT Leadership · Executive Briefing

Content Governance Risk — A Read-Only Audit Solution

Peabody Energy's Microsoft 365 estate spans 5 business units, 12+ mine operations across two continents, and a complex web of SharePoint sites, Teams channels, and OneDrive accounts. This Power BI solution gives Executive and IT leadership a real-time, read-only view of governance gaps, oversharing risk, and compliance exposure — without modifying a single document or permission.

The Five Risks We Are Closing

Critical

Anonymous & External Oversharing

Anonymous links on commercial contracts; never-expiring shares to unknown domains; zombie grants to inactive guests.

Critical

MNPI Exposure

Material Non-Public Information — earnings, M&A, forecast — stored without sensitivity labels or shared externally.

High

OneDrive Shadow Content

Sensitive files in personal OneDrive bypassing governed libraries; org-wide sharing of commercial documents.

High

Compliance & Retention Gaps

Safety records and permits without retention labels, violating MSHA / OSMRE / QLD DES record-keeping obligations.

Medium

Content Sprawl & Ungoverned Sites

Orphaned sites with no owner, libraries missing enterprise content types, base classification columns absent.

Medium

Custom Permission Drift

Item-level broken inheritance with non-standard ACLs and shadow-admin behaviour across high-value libraries.

Solution at a Glance

What It Does

Read-only audit and reporting layer. Five fact tables, six interactive report pages, one curated executive dashboard. No content or permissions are modified.

How It Works

SharePoint REST + Microsoft Graph APIs, called via Power Query (M) using a certificate-based Azure AD app registration. Loaded into a star-schema Power BI semantic model with DAX risk scoring.

Who Sees What

Four RLS roles with object-level masking: Executive (tenant-wide aggregates), IT Admin (full detail), Legal & Compliance, and BU Owner (filtered to their BU).

Implementation Footprint

9
Build Phases
Environment → Auth → API → Power Query → Model → Security → Reports → Deploy → Handover
73
Tracked Tasks
Independently actionable items, sequenced and assignable to the IT developer team
6
Report Pages
From single-page Executive Summary to full IT operational tracker
4
RLS Roles
Executive · IT Admin · Legal & Compliance · BU Owner — Entra-group bound

Licensing & Capability Matrix

CapabilityMinimum LicenseRecommendedStatus
Power BI report authoringPower BI Pro (per developer)ProIn place
Executive consumptionPower BI Pro / PPUPPU or FabricConfirm
Sensitivity labels on datasetMicrosoft Purview (E5)E5 tenant-wideConfirm
Row-Level Security enforcementPower BI Pro / PremiumPremiumIn place
Object-Level Security (column masking)Premium / Fabric CapacityFabric CapacityConfirm
Incremental refresh (4-hour permission deltas)Premium / FabricFabric CapacityConfirm
SharePoint REST + Graph API accessMicrosoft 365 E3/E5 (tenant)E5In place

Implementation Phase Timeline

W1 W2 W3 W4 W5 W6 W7+ 1. Environment Setup 2. App Registration & Auth 3. API Validation 4. Power Query Build (20 modules) 5. Semantic Model & DAX 6. RLS & OLS Security 7. Report Pages (6 pages) 8. Deploy & Validate 9. Handover PHASE Indicative duration — actual timing depends on tenant scale and governance approvals.
Phases 4–7 contain the bulk of the engineering work. Phases 1–2 are administrative gating items.
Risk if not implemented: Without this audit framework, governance posture remains assertion-based — there is no system of record showing where MNPI is exposed, which sites lack owners, or which external sharing links are active. Litigation, M&A due diligence, and regulator requests (MSHA, OSMRE, ASIC, SEC) all require defensible evidence.
Next steps for executive approval: (1) Confirm Premium / Fabric capacity availability. (2) Authorise SharePoint Global Admin to grant tenant consent for the read-only app registration. (3) Nominate the 4 RLS Entra security groups and their members. (4) Approve the IT developer assignment (estimated 1.5 FTE for build phases 4–7).
Section 1

Solution Overview & Architecture

Read-only audit and reporting layer over the Peabody M365 tenant. Five fact tables, six report pages, four RLS roles, and zero modifications to source content.

1.2 Solution Components

LayerTechnologyRole
Data CollectionSharePoint REST APISite, list, library, user, item permission inventory
Data CollectionMicrosoft Graph APISharing links, OneDrive items, Teams drives, guest identities
TransformationPower Query (M)API calls, pagination, token refresh, data shaping
Semantic ModelPower BI DatasetStar schema, DAX measures, RLS, calculated columns
ReportPower BI Reports6 interactive report pages (IT detail + exec summary)
DistributionPower BI AppCurated executive view, sensitivity-labeled dataset
AlertPower BI AlertsCritical threshold triggers (anonymous links, zero owners)
RefreshPower BI Gateway / FabricScheduled nightly full + 4-hour delta for permissions

1.3 Data Flow Architecture

PEABODY M365 TENANT SharePoint Online (REST) /_api/web · /_api/site · /_api/web/lists Microsoft Graph (v1.0) /sites · /drives · /users · /permissions AZURE AD APP-ONLY · CERTIFICATE AUTH client_credentials grant · 1h tokens · Azure Function proxy POWER QUERY (M) — TRANSFORMATION LAYER Module 1Site Discovery Module 2Library Profiler Module 3User & Permissions Pass 1/2/3 + KeywordsItem · Sharing · OneDrive POWER BI SEMANTIC MODEL · STAR SCHEMA 5 FACT TABLES Site · Library · ItemPerm · OneDrive · ExtShare 8 DIMENSION TABLES Site · User · BU · Mine · SensLabel · RiskTier · Date · ApprDomains DAX · RLS · OLS REPORT LAYER · 6 PAGES Pg 1 ExecSummary Pg 2 SiteHealth Matrix Pg 3 Ext &Anon Sharing Pg 4 OneDriveShadow Pg 5 BrokenInheritance Pg 6 RemediationTracker DISTRIBUTION · Power BI App (Exec) · Workspace (IT) · Data-Driven Alerts (CISO)
End-to-end data flow: tenant APIs → certificate auth → transformation → model → reports → governed distribution.

1.4 Scope

In Scope

  • All SharePoint Online sites in the Peabody tenant
  • Teams-connected SharePoint document libraries (all channels)
  • OneDrive for Business — priority roles first (Commercial, Finance, Legal), all users in baseline pass
  • External sharing grants and anonymous sharing links
  • Item-level broken permission inheritance
  • Site-level governance health (owners, labels, hub association)
  • Library-level classification health (CTs, columns, retention)

Out of Scope

  • SharePoint on-premises (legacy farms — separate workstream)
  • Power Automate remediation flows (Action Item #20)
  • SAP / Maximo integration (Action Item #8)
  • Email or Teams message content scanning
  • Writing or modifying any SharePoint content or permissions

1.5 Risk Tier Definitions

Critical
0d
SLA

Direct legal/regulatory/financial exposure. Anonymous link on commercial contract; MNPI without label.

High
30d
SLA

Significant compliance / security risk. Old external share, unlabeled site, safety library no retention.

Medium
90d
SLA

Governance gap weakening classification & discoverability. Library missing enterprise CT, no second owner.

Low
180d
SLA

Track and improve in next governance cycle. Subsite sprawl, stale libraries, naming inconsistencies.

Section 2

Prerequisites & Environment Setup

License confirmation, workstation requirements, workspace creation, SharePoint tenant settings, and the approved external domain reference list.

2.1 Required Licenses & Subscriptions

RequirementMinimum License / Plan
Power BI authoringPower BI Pro (per developer)
Power BI publishingPower BI Pro or Premium Per User (PPU)
Executive dashboard accessPower BI Pro or PPU (per consumer)
SharePoint API accessMicrosoft 365 E3/E5 (tenant-level)
Microsoft Graph API accessMicrosoft 365 E3/E5 (tenant-level)
Sensitivity labels on datasetMicrosoft Purview (M365 E5 or add-on)
Row-Level Security enforcementPower BI Pro or Premium
Scheduled refreshPower BI Pro + On-Prem Gateway or Fabric Capacity (recommended)
Incremental refreshPower BI Premium or Fabric Capacity
Note: Incremental refresh (delta-based OneDrive permission scans) requires Premium or Fabric. If neither is available at build time, implement full nightly refresh and revisit after licensing confirmed.

2.2 Developer Workstation Requirements

SoftwareVersionPurpose
Power BI DesktopMarch 2026+Report and model authoring
Power BI GatewayLatestOn-prem / SPO refresh
Visual Studio CodeLatestM query / DAX authoring
Azure CLILatestApp registration, cert mgmt
PowerShell 7+7.4+Cert generation, API tests
Postman (optional)LatestREST/Graph endpoint testing
Git clientLatestVersion control for .pbix

2.3 Power BI Workspace Setup

1

Create the workspace

Name: Peabody-GovernanceAudit · License: PPU or Fabric Capacity · Access: IT Admin (Admin), Developer team (Member)

2

Apply sensitivity label

Label: Confidential. Dataset contains user permission data and external sharing records — not for broad distribution.

3

Enable workspace settings

Large dataset storage format: ON · XMLA read/write endpoint: ON (advanced model ops) · Incremental refresh: ON (Premium / Fabric only)

2.4 SharePoint Tenant Settings (Admin Required)

SettingRequired State
SharePoint app-only access (/_layouts/15/appinv.aspx)Enabled
API access (Graph app permissions)Approved in SharePoint Admin
External sharing at tenant levelConfirm current setting (do not change — audit reads only)
Site collection app permissionsAllow app-only policy (tenant)

To enable tenant-level app-only in SharePoint Online, a SharePoint admin must run:

PowerShell
Set-SPOTenant -DisableCustomAppAuthentication $false
Note: In newer tenants this may already be managed via Azure AD app registrations only. Confirm with your SharePoint admin before running the command.

2.5 Approved External Domain Reference List

Used by the semantic model to classify external sharing grants as approved-partner vs. unknown-external. Stored as static CSV table DimApprovedDomains joined to FactExternalShares.

DomainPartner TypeNotes
aurizon.com.auRail carrier (AUS)QLD/NSW operations
pacificnational.com.auRail carrier (AUS)NSW operations
dbct.com.auPort terminal (QLD)DBCT
bnsf.comRail carrier (U.S.)PRB operations
up.comRail carrier (U.S.)PRB operations
haypoint.com.auPort terminal (QLD)Hay Point terminal
portwaratah.com.auPort terminal (NSW)Newcastle export
[Customer domains]Steel/power customersAdd from contract register before deploy
Section 3

Azure AD App Registration & Authentication

The single most critical setup step. All API calls authenticate using one app registration with certificate credentials. Do NOT use client secrets in production.

Authentication Flow

PowerShell Self-signed Cert .cer + .pfx + thumbprint Azure AD App Registration Peabody-GovernanceAudit-API Sites.Read.All · Files.Read.All User.Read.All · admin consented Token Endpoint /oauth2/v2.0/token grant=client_credentials Two scopes → two bearer tokens (1h) SharePoint REST {tenant}.sharepoint.com /.default scope Microsoft Graph graph.microsoft.com /.default scope Recommended production wrapper: Azure Function token proxy reads cert from Key Vault and returns bearer token to Power Query.
One app registration · two scopes · two short-lived tokens · no secrets in the .pbix.

3.1 App Registration — Step by Step

URL: https://portal.azure.com
Path: Azure Active Directory → App Registrations → New Registration

  • Name: Peabody-GovernanceAudit-API
  • Supported types: Accounts in this organizational directory only (single tenant)
  • Redirect URI: Leave blank (app-only flow, no user redirect)
  • Application (client) ID — used in all API calls
  • Directory (tenant) ID — used in token requests
  • Object ID — for reference
Store all three in Azure Key Vault. Never commit to Git.

Microsoft Graph — Application Permissions

  • Sites.Read.All — read all SharePoint site collections
  • Files.Read.All — read all files and OneDrive content
  • User.Read.All — read all user profiles (guest detection)

SharePoint — Application Permissions

  • Sites.Read.All — SharePoint REST API app-only read access
Critical: After adding permissions, click "Grant admin consent for [Peabody tenant]". Requires Global Admin or Application Admin. Without consent, all API calls return 403.

On the developer workstation, run in PowerShell 7+:

PowerShell
$cert = New-SelfSignedCertificate `
  -Subject "CN=Peabody-GovernanceAudit" `
  -CertStoreLocation "Cert:\CurrentUser\My" `
  -KeyExportPolicy Exportable `
  -KeySpec Signature `
  -KeyLength 2048 `
  -HashAlgorithm SHA256 `
  -NotAfter (Get-Date).AddYears(2)

$certPath = "C:\Peabody\Certs\GovernanceAudit.cer"
Export-Certificate -Cert $cert -FilePath $certPath

# Upload .cer (public) to App Registration → Certificates & Secrets
# Then export PFX (private key) for Azure Key Vault:
$pfxPath = "C:\Peabody\Certs\GovernanceAudit.pfx"
$pwd = ConvertTo-SecureString -String "[strong-password]" -Force -AsPlainText
Export-PfxCertificate -Cert $cert -FilePath $pfxPath -Password $pwd

# Record thumbprint for Power Query token requests:
$cert.Thumbprint
Cert expires in 2 years. Add a calendar reminder to 2028-04-22 for renewal (see Phase 9 Handover).

3.2 Token Acquisition Pattern for Power Query

Power Query does not natively support certificate-based OAuth. Three production-grade patterns:

Deploy a lightweight Azure Function (HTTP trigger) that:

  1. Loads the certificate from Azure Key Vault
  2. Requests a bearer token from Azure AD
  3. Returns the token to Power Query as JSON

Power Query calls the Azure Function URL with a managed identity or Function key. The token is injected into all API requests.

Why preferred: Decouples certificate from .pbix file; centralised renewal; server-side retry-after handling; no secrets ever land in Power BI.

Create a temporary client secret in the app registration for initial development and testing of Power Query queries only.

NEVER publish a .pbix with a client secret embedded. Replace with Option A before deploying to production.

If the tenant permits delegated SharePoint REST access via a service account, the Power BI Gateway can use a stored credential. Less preferred than app-only certificate auth — wider blast radius and harder to audit.

3.3 Token Request Parameters

ParameterValue
Grant typeclient_credentials
Token endpointhttps://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token
SharePoint scopehttps://{tenant}.sharepoint.com/.default
Graph scopehttps://graph.microsoft.com/.default
Token lifetime3600 seconds (1 hour)
Two separate tokens required — one per API surface — sharing the same app registration.
Section 4

Power BI Data Sources: SharePoint REST API

Four Power Query (M) modules — Site Discovery, Library Profiler, User & Permission Auditor, Item-Level Broken Inheritance Scan — each parameterised on SiteUrl and called iteratively across all sites.

4.1 Base REST Pattern

Critical: Always append application/json;odata=nometadata to the Accept header. Removes OData envelope noise and returns clean JSON — essential for Power Query parsing performance at scale.
Power Query M
let
  token   = // bearer from Azure Function proxy,
  headers = [
    #"Authorization" = "Bearer " & token,
    #"Accept"        = "application/json;odata=nometadata"
  ],
  url      = siteUrl & "/_api/web/...",
  response = Json.Document(Web.Contents(url, [Headers = headers]))
in
  response

4.2 Module 1 — Site Discovery

Pull identity and health properties for each site. Feeds FactSiteInventory and DimSite.

Response FieldSemantic Model Column
web/TitleSiteTitle
web/UrlSiteUrl
web/CreatedSiteCreatedDate
web/LastItemModifiedDateSiteLastModifiedDate
web/MembersCanShareMembersCanShare
site/Owner/EmailPrimaryOwnerEmail
site/StorageUsedStorageUsedMB
web/AssociatedOwnerGroup/UsersOwnerCount (calculated)
[Hub association via Graph]HubSiteId, IsHubAssociated
[Sensitivity label via Graph]SensitivityLabelId
[Teams connection via Graph]IsTeamsConnected, M365GroupId

REST Endpoints

REST
GET {siteUrl}/_api/web?$select=Title,Url,Created,LastItemModifiedDate,Description,MembersCanShare
GET {siteUrl}/_api/site?$select=Owner,StorageUsed,StorageUsageCurrent
GET {siteUrl}/_api/web/associatedownergroup/users?$select=Email,LoginName,Title

4.3 Pagination Handler (Mandatory)

SharePoint returns max 5,000 items per page (often only 100). You MUST handle @odata.nextLink or you will silently receive incomplete data.
Power Query M · fnGetAllPages
let
  GetAllPages = (url as text, token as text) =>
    let
      GetPage = (pageUrl as text, accumulated as list) =>
        let
          response = Json.Document(Web.Contents(pageUrl, [
            Headers = [
              Authorization = "Bearer " & token,
              Accept        = "application/json;odata=nometadata"
            ]
          ])),
          items    = response[value],
          nextLink = try response[#"odata.nextLink"] otherwise null,
          combined = accumulated & items,
          result   = if nextLink = null
                     then combined
                     else @GetPage(nextLink, combined)
        in result
    in GetPage(url, {})
in GetAllPages

4.4 Module 2 — Library Profiler

For each site, enumerate non-hidden lists/libraries and assess classification health. Feeds FactLibraryClassification.

REST
GET {siteUrl}/_api/web/lists
    ?$filter=Hidden eq false
    &$select=Title,BaseTemplate,ItemCount,ContentTypesEnabled,
              HasUniqueRoleAssignments,LastItemModifiedDate,Id
    &$expand=ContentTypes($select=Name),
              Fields($filter=Hidden eq false;$select=Title)

Base Column Presence Check (7 columns)

Power Query M
HasCol_BusinessUnit    = "Business Unit"     in Fields[Title]
HasCol_Mine            = "Mine"              in Fields[Title]
HasCol_Geography       = "Geography"         in Fields[Title]
HasCol_DocumentStatus  = "Document Status"   in Fields[Title]
HasCol_Confidentiality = "Confidentiality"   in Fields[Title]
HasCol_RetentionLabel  = "Retention Label"   in Fields[Title]
HasCol_OwnerApprover   = "Owner"             in Fields[Title]
BaseColumnScore        = // sum of 7 flags above (0–7)
BaseColumnGapCount     = 7 - BaseColumnScore

Enterprise Content Type Check

HasEnterpriseContentType = true if any ContentTypeName is NOT IN the OOTB set (Document, Folder, Item, Event, Announcement, Task, Link).

4.5 Module 3 — User & Permission Auditor

Pulls all users + permission assignments per site; identifies guests and broken inheritance at site level.

EndpointFields Extracted
/_api/web/siteusersEmail, LoginName, Title, IsSiteAdmin, UserId, UserType
/_api/web/sitegroups (expanded)Group Title, Group Type, User Email per group
/_api/web/roleassignments?$expand=Member,RoleDefinitionBindingsMember/LoginName, PrincipalType, RoleDefinitionBindings/Name

Guest Detection

M
IsExternalUser = Text.Contains(LoginName, "#EXT#") or UserType = "Guest"

Custom Permission Level Detection

M
IsCustomPermissionLevel = RoleDefinitionName not in
  {"Full Control","Design","Edit","Contribute",
   "Read","View Only","Limited Access",
   "Approve","Manage Hierarchy"}

4.6 Module 4 — Item-Level Broken Inheritance Scan (Pass 2)

Performance: Only run against libraries where LibraryHasBrokenInheritance = true from Module 2. Passing all libraries through this query will exceed throttle limits and create enormous, mostly empty result sets.
REST
GET {siteUrl}/_api/web/lists/getById('{listId}')/items
    ?$filter=HasUniqueRoleAssignments eq true
    &$select=Id,FileLeafRef,FileRef,Author/Email,
              Editor/Email,Modified,HasUniqueRoleAssignments
    &$expand=RoleAssignments/Member/Email,
              RoleAssignments/RoleDefinitionBindings,
              Author,Editor

Keyword Signal Matching (applied at this step)

Pseudocode
KeywordGroup =
  if any of [contract, agreement, renewal, pricing, PLV, HCC, PCI,
              nomination, charter, reopener] in FileName
    then "Commercial"
  else if any of [earnings, EBITDA, forecast, budget, guidance,
                   acquisition, Anglo, Moranbah, bid, LOI, NDA]
    then "Financial-MNPI"
  else if any of [reserve, resource, geological, seam, bore, JORC]
    then "Geological"
  else if any of [incident, MSHA, injury, fatality, LTI, permit,
                   reclamation, bond, compliance, violation]
    then "Safety-Regulatory"
  else if any of [laycan, vessel, demurrage, disruption, outage,
                   Black Warrior, lock, BNSF, Aurizon] in FileName
    then "Logistics"
  else "General"

KeywordHitFlag = KeywordGroup <> "General"
Section 5

Power BI Data Sources: Microsoft Graph API

Same Power Query pattern as REST but different token (scope https://graph.microsoft.com/.default) and base URL https://graph.microsoft.com/v1.0/. Three purposes: sharing-link resolution, OneDrive enumeration, site enrichment (hub / Teams / sensitivity).

5.2 Site Enrichment Query

Graph
GET /v1.0/sites?search=*&$select=id,displayName,webUrl,sharepointIds
GET /v1.0/sites/{siteId}?$select=id,displayName,webUrl,sensitivity,sharepointIds
GET /v1.0/sites/{siteId}/termStore

// Hub association: response includes isHubSite, hubSiteId properties
// isHubSite = false AND hubSiteId = null → ungoverned standalone

// Teams connection:
GET /v1.0/groups?$filter=resourceProvisioningOptions/any(x:x eq 'Team')
    &$select=id,displayName,resourceProvisioningOptions,sharepointIds

5.3 Teams Drive Enumeration

One drive per General + one per private/shared channel. Each Teams channel creates a separate document library backed by its own drive — enumerate all drives so no channel library is skipped in Pass 3.

Graph
GET /v1.0/sites/{siteId}/drives
    ?$select=id,name,driveType,createdBy,lastModifiedDateTime

5.4 Pass 3 — Sharing Link Resolution

For each item flagged in Pass 2 + all high-priority OneDrive items, retrieve the full permission object including link scope. Feeds FactExternalShares.

Graph
GET /v1.0/drives/{driveId}/items/{itemId}/permissions
    ?$select=id,roles,link,grantedTo,grantedToIdentities,
              inheritedFrom,hasPassword,expirationDateTime
FieldExtract / Detection
link/scope"anonymous" | "organization" | "users" | null
link/type"view" | "edit" | "review"
link/expirationDateTimenull = never expires (flag)
grantedTo/user/emailRecipient — extract domain, join DimApprovedDomains
inheritedFromnull = unique permission on this item
roles["read"] | ["write"] | ["owner"]
Detection logic
IsAnonymousLink   = link/scope = "anonymous"
IsOrgWideLink     = link/scope = "organization"
IsNeverExpires    = link/expirationDateTime = null and link <> null
IsApprovedDomain  = recipientDomain in DimApprovedDomains
IsExternalUnknown = IsExternalUser and not IsApprovedDomain

5.5 OneDrive Shadow Content Scan

For priority-role users, enumerate OneDrive items with external/org-wide sharing matching sensitive keyword patterns. Feeds FactOneDriveShadowContent.

GET /v1.0/users?$filter=accountEnabled eq true
    &$select=id,displayName,mail,department,jobTitle,userType

Filter to Department IN (Commercial, Marketing, Finance, Investor Relations, Legal, Compliance, Capital Projects, HSE).

GET /v1.0/users/{userId}/drive?$select=id,name,owner,lastModifiedDateTime,quota
// BASELINE (first run):
GET /v1.0/drives/{driveId}/root/search(q='')
    ?$select=id,name,size,webUrl,lastModifiedDateTime,file,parentReference
    &$top=1000

// INCREMENTAL — store delta token per drive:
GET /v1.0/drives/{driveId}/root/delta?token={savedDeltaToken}

Only pull permissions for items where KeywordHitFlag = true OR file/size > 0.

GET /v1.0/drives/{driveId}/items/{itemId}/permissions
OneDriveRiskTier =
  if IsAnonymousShare or (IsExternalShare and not IsApprovedDomain)
    then "Critical"
  else if IsExternalShare and IsApprovedDomain  then "High"
  else if IsOrgWideShare and KeywordHitFlag    then "High"
  else if IsOrgWideShare                       then "Medium"
  else                                          "Low"

5.6 Throttling — Mandatory Retry Pattern

At Peabody's scale (15+ sites, hundreds of users, potentially millions of permission objects), throttling WILL occur.
Power Query M
let
  CallWithRetry = (url as text, token as text, maxRetries as number) =>
    let
      Attempt = (n as number) =>
        let
          resp   = try Web.Contents(url, [
            Headers = [Authorization = "Bearer " & token],
            ManualStatusHandling = {429, 503}
          ]),
          status = Value.Metadata(resp)[Response.Status],
          result = if status = 429 and n < maxRetries
                   then @Attempt(n + 1)
                   else Json.Document(resp)
        in result
    in Attempt(0)
in CallWithRetry
Power Query M has no native sleep. For production, the Azure Function proxy (Section 3.2 Option A) handles Retry-After server-side before returning the response.
Section 6

Semantic Model Design

Standard star schema — five fact tables and eight dimension tables. All relationships one-to-many from dim → fact. No many-to-many (creates ambiguous filter contexts in DAX).

6.1 Star Schema Overview

PEABODY GOVERNANCE AUDIT — SEMANTIC MODEL FactSiteInventory FactLibraryClassification FactItemPermissions FactExternalShares FactOneDriveShadowContent DimSite DimBusinessUnit DimMine DimUser DimSensitivityLabel DimRiskTier DimDate DimApprovedDomains 1*
5 fact tables (gold) at the core; 8 dimension tables (navy) around the perimeter. All joins are 1:many (dim → fact).

6.2 Fact Table Schemas

ColumnTypeSourceNotes
SiteKeyIntSurrogatePK
SiteIdTextGraphGUID
SiteUrl, SiteTitleTextREST /_api/web
HubSiteId, IsHubAssociatedText/BoolGraphnull = ungoverned
IsTeamsConnected, M365GroupIdBool/TextGraph
SensitivityLabelIdTextGraphFK → DimSensLabel
OwnerCount, HasTwoOwnersInt/BoolREST /sitegroupsGap check
ExternalUserCount, HasExternalUsersInt/BoolREST /siteusersFlag
StorageUsedMBDecREST /_api/site
SiteCreatedDate, SiteLastModifiedDateDateREST /_api/web
DaysSinceModified, IsStaleInt/BoolDerived
BUKeyIntLookup on SiteTitleFK → DimBU
AuditRunDateDateLoaded at refresh
ColumnTypeSourceNotes
LibraryKey, SiteKey, ListId, LibraryTitleInt/Int/Text/TextSurrogate / FK / RESTPK / FK → DimSite
TemplateType, ItemCountIntREST lists101 = DocLib
ContentTypesEnabled, HasEnterpriseContentTypeBoolREST / DerivedGap check
LibraryHasBrokenInheritBoolREST listsGap check
BaseColumnScore, BaseColumnGapCountIntDerived0–7
HasRetentionLabelBoolREST / PurviewGap check
LibraryLastModifiedDateDateREST lists
GovernanceGapCountIntSum of gap flagsComposite score
LibraryRiskTierKeyIntDerivedFK → DimRiskTier
ColumnTypeSource
PermissionKey, LibraryKey, SiteKeyIntSurrogate / FKs
ItemId, FileName, FilePathTextREST items
CreatedByUserKey, LastModifiedByUserKeyIntFK → DimUser
LastModifiedDateDateREST items
PermissionedUserEmail, PermissionLevelTextREST roleassignments
IsExternalUser, IsCustomPermLevelBoolDerived
KeywordGroup, KeywordHitFlagText/BoolDerived
RiskTierKeyIntDerived → DimRiskTier
ColumnTypeSource
ShadowKey, UserKey, DriveId, ItemIdInt/Int/Text/TextSurrogate / FK / Graph
FileName, FilePath, FileSizeKBText/Text/DecGraph
IsAnonymousShare, IsOrgWideShare, IsExternalShareBoolGraph permissions
IsApprovedDomainBoolJoin DimApprDomains
ExternalRecipientEmail, SharingLinkUrlTextGraph permissions
LinkExpiryDate, IsNeverExpires, IsZombieLinkDate/Bool/BoolGraph / Derived
KeywordGroup, KeywordHitFlag, RiskTierKeyText/Bool/IntDerived
ColumnTypeSource
ShareKey, SiteKey, LibraryKey, SharedByUserKeyIntSurrogate / FKs
FileName, FilePathTextGraph / REST
SharingType, LinkScope, LinkType, SharingLinkUrlTextDerived / Graph
LinkCreatedDate, LinkExpiryDate, IsNeverExpiresDate/Date/BoolGraph / Derived
IsPasswordProtectedBoolGraph
RecipientEmail, RecipientDomainTextGraph / Derived
IsApprovedDomain, IsExternalUnknown, IsZombieLinkBoolDerived
KeywordGroup, KeywordHitFlag, DaysLinkActive, RiskTierKeyText/Bool/Int/IntDerived

6.3 Dimension Tables (Reference)

DimBusinessUnit

BUKeyBU NameShort CodeStrategic Posture
1Seaborne MetallurgicalSEA-METGrowth
2Seaborne ThermalSEA-THRStable Export
3Powder River BasinPRBCash Generation
4Other U.S. ThermalUS-THRCash Generation
5Corporate & OtherCORPSupport
99Unknown / UnclassifiedUNK

DimMine

MineBUGeographyType
Centurion1QLD, AUSUnderground
Coppabella1QLD, AUSSurface
Moorvale1QLD, AUSSurface
Metropolitan1NSW, AUSUnderground
Shoal Creek1AL, USAUnderground
Middlemount5QLD, AUSSurface (JV)
Wilpinjong2NSW, AUSSurface
Wambo2NSW, AUSMixed
NARM, Caballo, Rawhide3WY, USASurface
Illinois Basin4IL, USAMixed

DimSensitivityLabel

LabelWeightColor
Public1Green
Internal2Blue
Confidential3Yellow
Restricted (MNPI)4Red
Unlabeled5Dark Red

DimRiskTier

TierWeightSLA DaysColor
Critical40#CC0000
High330#FF6600
Medium290#FFCC00
Low1180#339933

6.4 Key DAX Measures

All measures live in a dedicated measure table (no columns). Prefix with [_] to sort to top of field list.

Governance KPI Measures

DAX
[_ Total Sites] = COUNTROWS(FactSiteInventory)

[_ Sites Without Sensitivity Label] =
  CALCULATE(COUNTROWS(FactSiteInventory),
            DimSensitivityLabel[LabelName] = "Unlabeled")

[_ % Sites Unlabeled] =
  DIVIDE([_ Sites Without Sensitivity Label], [_ Total Sites], 0)

[_ Sites Without Two Owners] =
  CALCULATE(COUNTROWS(FactSiteInventory),
            FactSiteInventory[HasTwoOwners] = FALSE)

[_ Libraries Without Enterprise CT] =
  CALCULATE(COUNTROWS(FactLibraryClassification),
            FactLibraryClassification[HasEnterpriseContentType] = FALSE)

[_ Libraries With Broken Inheritance] =
  CALCULATE(COUNTROWS(FactLibraryClassification),
            FactLibraryClassification[LibraryHasBrokenInherit] = TRUE)

Oversharing Measures

DAX
[_ Anonymous Links Active] =
  CALCULATE(COUNTROWS(FactExternalShares),
            FactExternalShares[LinkScope] = "anonymous")

[_ Unknown External Shares] =
  CALCULATE(COUNTROWS(FactExternalShares),
            FactExternalShares[IsExternalUnknown] = TRUE)

[_ Zombie Links] =
  CALCULATE(COUNTROWS(FactExternalShares),
            FactExternalShares[IsZombieLink] = TRUE)

[_ Never-Expiring Links] =
  CALCULATE(COUNTROWS(FactExternalShares),
            FactExternalShares[IsNeverExpires] = TRUE,
            FactExternalShares[LinkScope] <> "users")

[_ MNPI Files Shared Externally] =
  CALCULATE(COUNTROWS(FactExternalShares),
            FactExternalShares[KeywordGroup] = "Financial-MNPI")

[_ OneDrive Critical Risk Files] =
  CALCULATE(COUNTROWS(FactOneDriveShadowContent),
            DimRiskTier[RiskTier] = "Critical")

Composite Governance Risk Score

DAX
[_ Governance Risk Score] =
  VAR AnonymousWeight  = [_ Anonymous Links Active]        * 10
  VAR MNPIWeight       = [_ MNPI Files Shared Externally]   * 8
  VAR UnknownExtWeight = [_ Unknown External Shares]        * 6
  VAR ZombieWeight     = [_ Zombie Links]                   * 4
  VAR NoLabelWeight    = [_ Sites Without Sensitivity Label]* 5
  VAR NoOwnerWeight    = [_ Sites Without Two Owners]       * 3
  VAR NoCTWeight       = [_ Libraries Without Enterprise CT]* 2
  RETURN
    AnonymousWeight + MNPIWeight + UnknownExtWeight +
    ZombieWeight + NoLabelWeight + NoOwnerWeight + NoCTWeight
This is a directional risk indicator, not a compliance score. Weights tunable by CoE per Peabody's risk appetite — document any change in the quarterly policy review.
Section 7

Report Pages Design (6 Pages)

Six purpose-built pages with distinct audiences. Pages 1 and 3 are surfaced to executives via the Power BI App; Pages 1–6 are accessible to IT roles.

Page 1 — Executive Governance Summary

CIO · CFO · CLO · CISO · CEO
PEABODY ENERGY — GOVERNANCE RISK SUMMARY RISK SCORE847 CRITICAL12 ANON LINKS7 UNLABELED28 RISK HEATMAP BY BUSINESS UNIT TOP 10 CRITICAL FINDINGS GOVERNANCE SCORE TREND (30d)
Purpose: Single-page exec view, printable for board use.
Source: FactSiteInventory · FactExternalShares · FactOneDriveShadowContent · DimRiskTier
Notes: No filter pane · KPI ▲▼ vs prior audit run · Min 28pt KPI font

Page 2 — Site Health Matrix

IT Leadership · SP Admins · CoE
Slicers: BU | Hub | Teams | Risk Tier SiteBUHubTmsOwnSensCTColsRetExtBrk Centurion Commercial Hub Finance & IR Wilpinjong NARM Mine ⋮ (one row per site, filterable, drillthrough enabled) TOP 10 SITES BY GAP COUNT PASS / GAP / GAP≥3 874211
Purpose: Traffic-light status per site per governance rule.
Drillthrough: Red "Broken Inherit" → Page 5 · Red "External Users" → Page 3
RLS: BU owners filtered to their BU only.

Page 3 — External & Anonymous Sharing

CISO · Legal · IT Admin
Slicers: BU | Risk Tier | Link Scope | Keyword | Date ANON7 ORG-WIDE31 UNKNOWN14 MNPI3 ZOMBIE22 ∞ EXP68 EXTERNAL SHARES TABLE (drillthrough enabled) ⚠ contract-Q3.docx · anonymous · view · gmail.com earnings-FY26.xlsx · users · review · partner@xyz.com laycan-Q4.xlsx · organization · view · internal contract.pdf · users · view · bnsf.com (approved) nomination-Apr.docx · users · view · aurizon.com.au SHARES BY DOMAIN 90-DAY TREND KEYWORD × RISK
Purpose: Full inventory of external sharing with risk classification.
Notes: Anonymous rows red-shaded · MNPI rows get permanent ⚠ icon · CSV export bookmark for Legal litigation prep

Page 4 — OneDrive Shadow Content

IT Admin · Compliance · HR (limited)
Slicers: Department | Risk Tier | Keyword | Sharing Type USERS42 CRITICAL9 ANON OD4 MNPI OD2 USER RISK SCATTER · X=files · Y=ext shares · size=KB → files flagged FILES BY KEYWORD TOP USERS (anon for exec)
Purpose: User-level shadow content risk identification.
RLS: Exec sees department aggregation only — no user names · IT Admin sees full identity · HR has no access

Page 5 — Broken Inheritance Drill

IT Admin · SP Admins
Slicers: Site | BU | Keyword | Risk Tier | Date LIB W/BROKEN38 ITEMS BROKEN412 EXT ACCESS29 CUSTOM ROLE17 HIERARCHICAL: Site → Library → File ▾ Commercial Hub ▾ Contracts & Pricing (24 broken) contract-Q3.docx · J.Smith · custom · ext access ⚠ PLV-pricing.xlsx · M.Doe · Edit · MNPI ▾ Finance & IR ▾ M&A Workspace (12 broken) Anglo-LOI-draft.docx · K.Chen · Owner · ext gmail.com ⚠ LIBS BY ITEMS SHADOW ADMINS SITE → USER → LEVEL
Purpose: Itemised view of every document with broken inheritance — who broke it, when, what effect.
Drillthrough: back to Page 2 (Site Health) · Red flag where custom permission gives non-Peabody user Owner/Edit

Page 6 — Remediation Tracker

IT Operations · CoE
Slicers: BU | Risk | Status | Assignee | Due OPEN CRIT12 OPEN HIGH38 OVERDUE7 RESOLVED 7d21 % COMPLETE68% ACTION TABLE (write-back to SP list via Power Automate) #001 · Anon · contract-Q3 · Crit · J.Smith · 2026-04-22 · Open #002 · OneDrive · earnings.xlsx · Crit · K.Chen · 2026-04-22 · In Prog #003 · Site Gap · Wilpinjong owners · High · M.Doe · 2026-05-22 · Open #004 · Lib Gap · NARM CT · Med · L.Park · 2026-07-21 · Resolved #005 · Broken · Anglo-LOI · Crit · J.Smith · OVERDUE BURNDOWN (30d) OPEN BY ASSIGNEE
Status values: Open · In Progress · Escalated · Resolved · Accepted Risk
Notes: Status & Assignee write back to SP list via Power Automate · SLA = LinkCreatedDate + DimRiskTier[SLADays] · Overdue when SLA < TODAY()
Section 8

Executive Dashboard & Power BI App

A pinned-tile dashboard surfaced through a published Power BI App — the only artefact most executives ever see. Optimised for at-a-glance daily review on desktop or Power BI mobile.

8.1 Dashboard Tile Layout

PEABODY ENERGY · GOVERNANCE EXECUTIVE DASHBOARD Last refresh: 06:00 CT · Audit run: 18 days remaining GOVERNANCE RISK SCORE847▲ +12 CRITICAL FINDINGS12▲ +3 ANONYMOUS LINKS7▼ -2 UNLABELED SITES28▼ -1 RISK SCORE TREND (90 DAYS) target ≤ 800 RISK BY BUSINESS UNIT SEA-MET · 312 CORP · 218 SEA-THR · 154 MNPI EXPOSURE3files · ext shared SITES WITHOUT 2 OWNERS14of 127 total sites REMEDIATION SLA68%on-time vs target 85% VIEW FULL REPORT →Drill into Pages 1–6workspace · GovernanceAudit
Pinned tiles auto-refresh from underlying semantic model · single click to drill into corresponding report page.

8.2 Power BI App Configuration

SettingValue
App NamePeabody Governance Audit
DescriptionQuarterly governance health, oversharing exposure, remediation tracking
LogoPeabody Energy navy/gold (Brand Hub asset · 256×256 PNG)
Theme ColorNavy #1a2e5a primary · Gold #e8920a accent
Audience: ExecutivesPage 1 + Page 3 only · CEO, CFO, CIO, CLO, CISO, BU Presidents
Audience: IT LeadershipAll pages · Director IT, Manager Modern Workplace
Audience: SP AdminsPages 2, 5, 6 · Operational team
Audience: Compliance / LegalPages 3, 4 · Restricted to non-PII view (RLS)

8.3 Subscription & Alert Configuration

AlertThresholdRecipientsFrequency
Critical findings spike+5 from prior runCIO, CISO, IT DirectorReal-time email
New anonymous link detectedAny new anonymous linkCISO, IT Admin teamReal-time email + Teams card
MNPI external sharingAny new MNPI keyword external shareCFO, CLO, CISOReal-time email (high priority)
Risk score breachScore > 1,000 (action threshold)Executive Steering CommitteeSame-day email
Page 1 weekly digestExecutive distributionMonday 07:00 CT (PDF)
Real-time alerts via Power Automate flow watching the dataset; Teams cards posted to #governance-alerts channel for IT operational visibility.
Section 9

Row-Level & Object-Level Security

The audit dataset itself contains sensitive metadata (file names, user identities, sharing exposure). RLS and OLS apply least privilege so each audience sees only what their role requires.

9.1 Role & Access Matrix

RolePage 1 ExecPage 2 SitesPage 3 ExternalPage 4 OneDrivePage 5 BrokenPage 6 RemediationUser Identities
Executive (CEO/CFO/CIO/CLO)YESNOPARTIALNONOSUMMARYMASKED
BU PresidentOWN BUOWN BUOWN BUNOOWN BUOWN BUDEPT ONLY
CISO / IT SecurityYESYESYESYESYESYESFULL
SP Admin / IT OperationsYESYESYESYESYESYESFULL

9.2 RLS Filter — DAX

DAX · Role: BU President
// On DimBusinessUnit table
[BUName] = LOOKUPVALUE(
  DimUserBUMapping[BUName],
  DimUserBUMapping[UserPrincipalName], USERPRINCIPALNAME()
)
DAX · Role: Executive (no BU restriction; only summary data)
// On DimUser table — mask individual identities for execs
[UserPrincipalName] <> USERPRINCIPALNAME() &&
  FALSE()  // blocks user-level rows; aggregations roll up safely
Test all RLS roles using Power BI Desktop → Modeling → View As Roles before publishing. Cross-filter direction on FactItemPermissions must be Single (dim → fact) — bidirectional filters break RLS in unpredictable ways.

9.3 Object-Level Security (Column Masking)

Apply via Tabular Editor (free tool, OLS not supported in Power BI Desktop GUI). Hide columns from named roles entirely so they cannot be referenced in DAX, exports, or Q&A.

Table.ColumnHidden From RoleReason
FactOneDriveShadowContent[FileName]ExecutiveFile names may reveal MNPI
FactOneDriveShadowContent[FilePath]ExecutivePath discloses project codenames
DimUser[Email] & [DisplayName]ExecutiveAvoid exec → individual punitive action; aggregate views only
FactExternalShares[RecipientEmail]BU President (other BUs)Cross-BU exposure not in BU scope
FactItemPermissions[FileName]ExecutiveNames disclose deal codenames
Section 10

Refresh Strategy & Performance

Three-tier refresh: full quarterly baseline + weekly incremental + daily delta on highest-risk subset. Total cycle stays under 6 hours with throttle-aware patterns.

10.1 Refresh Policy

TierScopeScheduleMechanismApprox Duration
Quarterly FullAll facts & dims · all sites · all libraries · all items1st Mon of Q · 22:00 CTManual trigger (CoE)4–6 hours
Weekly IncrementalModified items only via Graph delta tokensMon 03:00 CTScheduled refresh45–90 min
Daily Delta (High-Risk)Sites flagged Critical or High · external/anon links · MNPI keywordsDaily 06:00 CTScheduled refresh (separate dataflow)10–20 min
Real-time AlertsNew anonymous links · MNPI external sharesPush (Graph subscriptions)Power Automate flow< 5 min latency
REFRESH SCHEDULE — DAILY CADENCE 00:00 03:00 06:00 12:00 18:00 22:00 Incremental Daily Δ Dashboard Live Real-time alerts (Graph subscriptions)

10.2 Incremental Refresh Configuration

Power Query · RangeStart / RangeEnd parameters
let
  Source         = SharePointAuditTable,
  FilteredByDate = Table.SelectRows(Source, each
    [LastModifiedDate] >= RangeStart and
    [LastModifiedDate] <  RangeEnd)
in
  FilteredByDate
SettingValue
Store data starting5 years before refresh date
Incrementally refresh30 days before refresh date
Detect data changesUse LastModifiedDate column
Refresh complete days onlyYes

10.3 Graph Delta Token Pattern

1
Initial baseline callGET /drives/{driveId}/root/delta with no token. Returns all items + a delta token.
2
Persist the delta token per drive in a SharePoint list (or Azure Table). Key: driveId, value: deltaToken, lastSync.
3
Subsequent calls use the saved token: GET /drives/{driveId}/root/delta?token={savedDeltaToken} — returns only changed items.
4
Update token with each response's new @odata.deltaLink after successful processing.
Delta tokens reduce Graph calls by 95–99% after baseline. Plan for 90-day token expiry — fallback to full refresh on token rejection (HTTP 410 Gone).

10.4 On-Premises Data Gateway

Required because the Azure Function token proxy lives in Peabody's Azure tenant and must be reachable from Power BI Service via secure channel.

ComponentConfiguration
Gateway typeStandard mode (cluster of 2 for HA)
Service accountDomain account with logon-as-service · KeyVault read scope
Data sourceWeb data source pointing to Azure Function URL
AuthenticationAnonymous (Function key passed in URL · key rotated quarterly)
Privacy levelOrganizational
Section 11

Deployment Pipeline

Three-stage Power BI deployment pipeline (Dev → Test → Prod) with parameter rules so the same .pbix promotes cleanly without re-pointing data sources manually.

11.1 Pre-Deployment Checklist

  • Model file size < 1 GB (current ~120 MB target)
  • All measures documented with description (Tabular Editor → Properties → Description)
  • No unused columns visible to end users (Hide from Client Tools)
  • All RLS roles tested via "View As Roles"
  • OLS roles validated via Tabular Editor → "Test as Role"
  • Refresh succeeds end-to-end on Test workspace gateway
  • Performance Analyzer: no visual > 3,000 ms render time
  • DAX queries reviewed by CoE — no FILTER(ALL(table)) in measures (perf killer)
  • Theme JSON file matches Peabody brand tokens

11.2 Deployment Steps

1
Build in Dev workspace — connected to Dev SharePoint sites (subset of 2–3 pilot sites) and Dev Azure Function.
2
Promote Dev → Test via deployment pipeline. Apply parameter rules: SiteUrlBase, FunctionAppUrl, TenantId swap to Test values.
3
UAT in Test — IT leadership + 2 BU governance leads + Legal + 1 SP Admin. 2-week sign-off window.
4
Promote Test → Prod via pipeline · parameters swap to Prod.
5
Configure Prod refresh schedule — gateway · credentials · alerts · subscriptions.
6
Publish Power BI App with audience-based pages (Section 8.2).
7
Distribute & train — exec briefing, IT leadership walkthrough, SP admin operational training, Legal/Compliance scenario walk-through.

11.3 Source Control & Commit Conventions

Power BI Project (.pbip) format committed to Azure DevOps Git repo. One commit per logical change. Conventional Commits style.

Examples
feat(model):    add FactOneDriveShadowContent table
fix(rls):       correct BU President filter cross-direction
perf(query):    use delta token for OneDrive enumeration
docs(measure):  add description for [_ Governance Risk Score]
chore(theme):   update gold accent to brand-compliant #e8920a
Section 12

Implementation Master Checklist

Authoritative sequence for the developer team — 73 independently actionable tasks across 9 phases. Tasks marked PARALLEL may be executed concurrently with the prior task. Progress is saved to your browser's local storage.

Overall Progress

0 of 73 tasks complete
0% · 0 / 9 phases
Phase 1 — Environment Setup
0 / 5
1
Confirm Power BI Premium Per User or Fabric Capacity license
Assigned to all developer accounts
2
Create workspace "Peabody-GovernanceAudit"
Premium / Fabric license mode
3
Apply "Confidential" sensitivity label to workspace
4
Confirm SharePoint Online tenant setting
Set-SPOTenant -DisableCustomAppAuthentication $false (run as SharePoint Global Admin)
5
Confirm M365 E3/E5 licenses enable Graph scopes
Sites.Read.All and Files.Read.All
Phase 2 — App Registration & Auth
0 / 9
6
Create Azure AD app registration
Name: Peabody-GovernanceAudit-API · Tenant: Single-tenant only
7
Record & store Application ID + Tenant ID securely
8
Add API permissions per Section 3.1 Step 4
Graph: Sites.Read.All, Files.Read.All, User.Read.All · SharePoint: Sites.Read.All
9
Grant admin consent for all permissions
Requires Global Admin or Application Admin
10
Generate certificate via PowerShell
Section 3.1 Step 5 · Store PFX in Azure Key Vault
11
Upload .cer public key to app registration
12
Record & store certificate thumbprint
13
Deploy Azure Function Token Proxy (Option A)
Section 3.2 — or configure client secret for DEV only
14
Test token acquisition for both scopes
SharePoint: https://{tenant}.sharepoint.com/.default · Graph: https://graph.microsoft.com/.default · Verify HTTP 200 with valid bearer token via Postman or Invoke-RestMethod
Phase 3 — API Endpoint Validation
0 / 4
15
Test SharePoint REST endpoints against pilot site
Centurion or Commercial Hub: /_api/web · /_api/web/lists · /_api/web/siteusers · /_api/web/roleassignments
16
Test Graph endpoints on pilot site
/v1.0/sites/{siteId} · /v1.0/sites/{siteId}/drives · /v1.0/users — verify pagination markers
17
Test item-level permission endpoint
Library with at least one item with HasUniqueRoleAssignments = true
18
Test Graph sharing link endpoint
Known file with active link · verify link/scope field present
Phase 4 — Power Query Build
0 / 20
19
Create PeabodyGovernanceAudit.pbix
20
Create Power Query parameters
TenantId · ClientId · TenantSharePointUrl · TokenProxyUrl · AuditRunDate (default TODAY())
21
Create token acquisition function fnGetToken
22
Create pagination helper fnGetAllPages
Section 4.3 · test against /_api/web/lists
23
Build Module 1 (Site Discovery) on pilot site
Shape to FactSiteInventory schema
24
Build site list parameter / table
All in-scope sites · start with Critical-tier sites
25
Convert Module 1 to function fnGetSiteInventory
Apply across all sites in site list
26
Build Module 2 (Library Profiler) as fnGetLibraries
Content type expand + base column presence check
27
Apply Module 2 to all sites
Verify item counts & CT name parsing
28
Build Module 3 (User & Permission Auditor) as fnGetSiteUsers
Apply guest detection (#EXT# filter)
29
Build Pass 1 filter
Libraries where LibraryHasBrokenInheritance = true → input list for Pass 2
30
Build Module 4 / Pass 2 fnGetBrokenItems
Apply keyword signal matching on FileName
31
Build Graph Site Enrichment fnGraphSiteInfo
Hub, Teams, sensitivity label · join to FactSiteInventory
32
Build Graph Drive Enumeration fnGetSiteDrives
33
Build Pass 3 fnGetSharingLinks
Extract link/scope, link/type, grantedTo, expiry · domain classification join
34
Build OneDrive user targeting query
Filter to priority departments · output user list with DriveId
35
Build OneDrive item scan fnGetODItems
Graph delta or search per drive · keyword match on filename
36
Build OneDrive permission resolution fnGetODPermissions
Apply risk tier logic
37
Build DimApprovedDomains static table
From Section 2.5 domain list
38
Build DriveTokenStore read/write queries
Delta token management — Section 10.3
Phase 5 — Semantic Model Build
0 / 7
39
Load all Power Query outputs as dataset tables
Verify row counts non-zero & schemas match Section 6.2
40
Create surrogate key columns for each fact table
41
Build all dimension tables per Section 6.3
DimBU, DimMine, DimRiskTier, DimDate, DimSensLabel, DimUser, DimApprovedDomains
42
Define all relationships per Section 6.1 star schema
All active · 1:* dim → fact
43
Create all calculated columns per Section 6.2
IsAnonymousLink, IsZombieLink, KeywordGroup, KeywordHitFlag, RiskTierKey, etc.
44
Create all DAX measures per Section 6.4
Test each against pilot data
45
Create BUOwnerMapping table
Section 9.2 · current BU presidents + delegate emails
Phase 6 — RLS & Security
0 / 4
46
Define all four RLS roles in Power BI Desktop
Modeling → Manage Roles per Section 9.1
47
Test each RLS role using "View as Role"
Executive · BU_Owner · IT_Admin · Legal_Compliance
48
Configure OLS in Tabular Editor via XMLA endpoint
Hide email/URL columns from Executive and BU_Owner roles
49
Map Entra ID security groups to RLS roles in Power BI Service
Confirm group memberships in Azure AD
Phase 7 — Report Build
0 / 8
50
Build Page 1 — Executive Governance Summary
Apply branding · test KPI cards, heatmap, trend
51
Build Page 2 — Site Health Matrix
Verify red/yellow/green conditional formatting
52
Build Page 3 — External & Anonymous Sharing
Verify drillthrough · test CSV export bookmark
53
Build Page 4 — OneDrive Shadow Content
Test scatter plot, ring chart, drillthrough
54
Build Page 5 — Broken Inheritance Drill
Test hierarchy expand · drillthrough back to Page 2
55
Build Page 6 — Remediation Tracker
Create SP list · configure Power Automate write-back · verify SLA flags
56
Configure mobile layout for Page 1
View → Mobile layout
57
Set sensitivity labels on all report pages and dataset
Phase 8 — Deployment & Validation
0 / 13
58
Complete pre-deployment checklist (Section 11.1)
59
Publish .pbix to Peabody-GovernanceAudit workspace
60
Configure dataset credentials in Power BI Service
Service principal + certificate
61
Run a manual refresh in Power BI Service
Verify completion · check post-refresh row counts
62
Configure scheduled refresh per Section 10.1
Set failure notification email to IT Admin team
63
Configure incremental refresh policy
FactItemPermissions & FactExternalShares · Section 10.2
64
Create Power BI App per Section 8.3
Configure audience groups and navigation
65
Pin dashboard tiles & configure data-driven alerts
Sections 8.2 + 8.4
66
Assign Entra groups to RLS roles in Service
Confirm correct view per role before go-live
67
Conduct IT leadership walkthrough of Pages 1–6
Document & apply requested changes
68
Conduct executive walkthrough of the Power BI App
Pages 1 and 3 · confirm KPI legibility & heatmap clarity
69
Commit final .pbix to version control
Tag release: v1.0.0-production
70
Document approved external domain list
IT knowledge base · schedule quarterly CoE review
Phase 9 — Handover & Ongoing Operations
0 / 3
71
Create developer handover documentation
Token Proxy maintenance · cert renewal (2yr — calendar now) · delta token refresh · adding new sites/domains · BUOwnerMapping updates
72
Schedule quarterly CoE governance review
Keyword groups · approved domains · RLS assignments · risk weights · SLA thresholds
73
Create runbook for alert response
Recipients · escalation path (CISO → Legal → IT Admin) · SLA · resolution recording in Remediation Tracker