Disclaimer: This is a sample/reference implementation provided "AS IS" without warranty. It is not an official Microsoft product or service and is not supported by Microsoft Support. See NOTICE.md for full details.
This project grew out of a recurring request from customers: they wanted a single pane of glass to see all the Microsoft updates that affect them — Power Platform release plans, the Microsoft 365 roadmap, Azure announcements, tenant-specific Message Center posts, and Service Health incidents — without bouncing between half a dozen portals, RSS feeds, and admin centers. Something they could deploy internally on their own infrastructure or host in Azure. The Microsoft Communications Portal brings those streams together into one filterable, dark-mode-friendly UI backed by a small Node.js server that proxies the upstream APIs and handles Microsoft Graph authentication.
If you wire up the Azure Management API as well, this same portal can surface Azure Service Health and Azure Resource Health events alongside the Microsoft 365 data, giving you a complete view of platform-side communications and tenant-side incidents in one place.
| Page | Route | Source file | Backend |
|---|---|---|---|
| Home | /home |
home.html |
— (dashboard/landing page) |
| Power Platform Release Planner | /powerplatform |
powerplatform.html |
releaseplans.microsoft.com (proxied) |
| Regional Release Plans | /featuregeo |
featuregeo.html |
releaseplans.microsoft.com (scraped + normalised) |
| Microsoft 365 Roadmap | /m365updates |
m365updates.html |
M365 Roadmap RSS |
| Azure Updates | /azureupdates |
azureupdates.html |
Azure Updates RSS |
| Microsoft Fabric Roadmap | /fabricroadmap |
fabricroadmap.html |
roadmap.fabric.microsoft.com (proxied) |
| Microsoft 365 Message Center | /messagecenter |
messagecenter.html |
Microsoft Graph |
| Microsoft 365 Service Health | /servicehealth |
servicehealth.html |
Microsoft Graph |
| Azure Service Health | /azureservicehealth |
azureservicehealth.html |
Azure Management API (ARM) |
| Guided Report | /guidedreport |
guidedreport.html |
— (multi-source report wizard) |
Every page supports light and dark themes. Pass ?clawpilotTheme=light or
?clawpilotTheme=dark on the URL, or click the theme toggle in the header.
The site root (/) redirects to /home.
Seven of ten pages work with zero credentials — you can be up and running in under a minute. The remaining pages need extra setup, and each is optional:
| Pages | What you need | Who should care |
|---|---|---|
| Home, Power Platform Release Planner, M365 Roadmap, Azure Updates, Fabric Roadmap, Guided Report | Nothing — works immediately | Everyone |
| Message Center, Service Health | An Entra app registration that can read your tenant's Microsoft Graph data (see Setup step 2) | IT admins who want tenant-specific M365 announcements and incident reports |
| Azure Service Health | The same Entra credentials plus an Azure role assignment (see Setup step 3) | Teams that also monitor Azure subscription-level health events |
| AI Insights (available on seven feed pages) | An API key from Azure OpenAI, OpenAI, or GitHub Models (see AI summarization) | Anyone who wants AI-generated summaries and "top 5 most impactful changes" digests |
Not sure where to start? Pick the deployment option below that matches your situation. You can always add Graph credentials, Azure Service Health, or AI later.
What this does: Deploys the portal as an Azure App Service (a managed web server in your Azure subscription) with a single command. The infrastructure (server, networking, etc.) is created automatically from the included Bicep templates.
Best for: Teams that want a shared, always-on instance hosted in Azure without managing servers manually.
Prerequisites:
- Azure Developer CLI (
azd) — a command-line tool for deploying apps to Azure - PowerShell 7+
(
pwsh) — needed for the post-deploy script that sets up Graph permissions - An Azure subscription with permission to create resources
azd init --template russrimm/MicrosoftCommunicationsPortal
azd upAfter azd up finishes, the post-provision hook will offer to set up Graph
permissions for Message Center and Service Health automatically.
Enable Entra ID authentication for shared deployments: Create or reuse a single-tenant Entra app registration, copy its Application (client) ID, and set it before deployment:
azd env set AUTH_CLIENT_ID <your-entra-app-client-id>
azd env set AUTH_CLIENT_SECRET <your-entra-app-client-secret> # recommended
azd upThe post-provision hook reuses that app and registers
<app-url>/.auth/login/aad/callback. Easy Auth then redirects unauthenticated
visitors to organizational sign-in. If AUTH_CLIENT_ID is omitted, the seven
public pages remain available, but tenant-specific Graph/ARM and billed AI API
routes reject requests because the application refuses to trust Easy Auth
headers unless App Service reports that authentication is enabled.
AUTH_CLIENT_SECRET is optional but recommended. When set, the deployment
stores it in the MICROSOFT_PROVIDER_AUTHENTICATION_SECRET app setting and Easy
Auth acts as a confidential client using the hybrid flow
(response_type=code id_token), exchanging the authorization code server side.
Without it, Easy Auth falls back to the implicit flow
(response_type=id_token), which returns the ID token directly in the redirect.
Production Easy Auth: Supplying
AUTH_CLIENT_SECRETcovers the confidential-client requirement, but a client secret still expires and has to be rotated. For production, Microsoft recommends a user-assigned managed-identity federated credential instead; see Use a managed identity instead of a secret.
What this does: Serves the HTML, CSS, and JavaScript from Azure Static Web
Apps' global edge network, and proxies every /api/* request to the same
server.js App Service that Option 1 deploys. The App Service keeps its managed
identity, so Microsoft Graph and Azure Resource Manager calls are unchanged.
Best for: Teams that want edge-cached static assets and Static Web Apps' built-in Microsoft Entra ID sign-in in front of the existing API.
Browser ──> Static Web Apps (Standard)
├── static content + managed Entra ID auth
└── /api/* ──proxy──> linked App Service (server.js)
└── managed identity ──> Graph + ARM
Prerequisites:
- Everything from Option 1 — this option adds a Static Web App in front of the App Service, it does not replace it.
- The Standard plan. Linked backends are not available on the Free plan.
- An Entra app registration with a client secret, because Static Web Apps' custom Entra ID provider does not support managed identity.
azd env set DEPLOY_STATIC_WEB_APP true
azd env set STATIC_WEB_APP_LOCATION eastus2 # or westus2, centralus, westeurope, eastasia
azd env set AUTH_CLIENT_ID <your-entra-app-client-id>
azd env set AUTH_CLIENT_SECRET <your-entra-app-client-secret>
azd upProvisioning creates the Static Web App, links the App Service as its /api
backend, and sets AUTH_MODE=swa on the App Service so it trusts the
x-ms-client-principal header that Static Web Apps forwards.
Then set up content deployment. Static content is published by the
azure-static-web-apps.yml
workflow. Grab the deployment token and store it as a repository secret:
az staticwebapp secrets list --name <static-web-app-name> \
--query "properties.apiKey" -o tsv- Repository secret
AZURE_STATIC_WEB_APPS_API_TOKEN— the token above. - Repository variable
ENTRA_TENANT_ID— your tenant ID. Without it the build drops the custom Entra registration and falls back to the preconfigured multi-tenant provider.
To preview the payload locally:
npm run build:swa # writes dist/
npx @azure/static-web-apps-cli start dist --swa-config-location dist \
--api-devserver-url http://127.0.0.1:3000Why a build step?
server.jsinjects a per-request CSP nonce into every inline<script>. Static Web Apps serves HTML straight from storage, so no nonce can be generated.scripts/build-swa.jsreplaces it with build-timesha256CSP hashes, which keeps the policy strict withoutunsafe-inline.
Linking security: Linking the backend automatically adds an App Service identity provider named Azure Static Web Apps (Linked) that rejects traffic not proxied through the Static Web App. Unlinking does not remove it, so the App Service is never left anonymously exposed. Because of this, the bicep template skips its own Easy Auth configuration when
DEPLOY_STATIC_WEB_APPistrue— otherwise a redeploy would overwrite the linked provider and break the connection.
Pull request previews are not supported. Static Web Apps does not attach linked backends to preview environments, so the workflow deploys only on push to
mainand on manual dispatch.
What this does: Runs the portal in a Docker container — a lightweight, self-contained package that includes everything the app needs. No need to install Node.js or other tools on your machine.
Best for: Trying the portal quickly, running on an existing Docker host, or deploying on-premises where Azure isn't available.
Prerequisite: Docker Desktop (Windows/Mac) or Docker Engine (Linux).
Pull and run:
docker run -p 127.0.0.1:3000:3000 ghcr.io/russrimm/microsoftcommunicationsportal:latestOr build from source:
docker build -t mcp .
docker run -p 127.0.0.1:3000:3000 mcpOpen http://localhost:3000. Seven pages work immediately — no credentials needed.
Security note: The examples above bind to
127.0.0.1(loopback), so only your local machine can reach the portal, andAUTH_MODEis auto-inferred asnone-loopback-only. Using-p 3000:3000(or any non-loopback publish) instead exposes the container on all network interfaces — the server will refuse to start unless you also setAUTH_MODE=reverse-proxy(oreasyauth) plusAPI_AUTH_TOKENandALLOW_REMOTE_BIND=true. See Authentication modes for the full matrix.
For Graph-backed pages (Message Center, Service Health), pass credentials as environment variables:
docker run -p 127.0.0.1:3000:3000 \
-e AZURE_TENANT_ID=your-tenant-id \
-e AZURE_CLIENT_ID=your-client-id \
-e AZURE_CLIENT_SECRET=your-client-secret \
mcpWhere do these values come from? See Setup step 2 below — it walks you through creating the Entra app registration that produces these three values.
What this does: Clones the source code and runs the portal directly on your machine using Node.js. You can edit the code and see changes immediately.
Best for: Developers who want to customize the portal, contribute code, or just kick the tires without Docker or Azure.
Prerequisites:
| Prerequisite | Version | Check | Install |
|---|---|---|---|
| Node.js (LTS) | 24.x or later | node -v |
nodejs.org/download |
| npm | Bundled with Node | npm -v |
Comes with Node.js |
| Git | Any recent | git --version |
git-scm.com |
| PowerShell 7+ (optional) | 7.x | pwsh -v |
Install PowerShell |
| Azure CLI (optional) | Any recent | az --version |
Install Azure CLI |
Node.js note: The server uses the built-in
http,https,crypto, andURLAPIs with no native add-ons, so any Node 24 LTS build (x64, ARM64, etc.) works. Earlier versions (18–22) may work but are not tested.Optional tools: PowerShell 7+ and Azure CLI are only needed if you want to run
scripts/create-entra-app.ps1to set up Graph API credentials for the Message Center and Service Health pages.
Clone and run:
git clone https://github.com/russrimm/MicrosoftCommunicationsPortal.git
cd MicrosoftCommunicationsPortal
npm install
npm start # production mode
# or
npm run dev # watch mode — auto-restarts on file changesOpen http://localhost:3000. Seven of ten pages work immediately with no credentials.
For Graph-backed pages (Message Center, Service Health), copy .env.example to .env and fill in your Entra app credentials — or run pwsh scripts/create-entra-app.ps1 to automate it.
Nine of the ten pages are shown in light and dark mode below (18 screenshots).
The Feature Geography page does not yet have checked-in screenshots. Regenerate with
node scripts/capture-screenshots.js (Playwright) while the server is running
on http://localhost:3000. The script visits each route with both theme query
strings, waits for any visible "Loading…" banner to clear (up to 45 s, since
the Power Platform page fans out ~20 upstream calls on a cold cache), and writes
1440×900 × 2 DPR PNGs to screenshots/.
| Light | Dark |
|---|---|
![]() |
![]() |
| Light | Dark |
|---|---|
![]() |
![]() |
| Light | Dark |
|---|---|
![]() |
![]() |
| Light | Dark |
|---|---|
![]() |
![]() |
| Light | Dark |
|---|---|
![]() |
![]() |
| Light | Dark |
|---|---|
![]() |
![]() |
| Light | Dark |
|---|---|
![]() |
![]() |
| Light | Dark |
|---|---|
![]() |
![]() |
| Light | Dark |
|---|---|
![]() |
![]() |
For a faster setup, see Quick Deploy above.
npm installThis downloads the one runtime dependency (dotenv) and is required before the
server can start.
(Required only for the Message Center, Service Health, and Azure Service Health pages. Skip this if you only need the seven public pages.)
The Message Center and Service Health pages pull data from Microsoft Graph — Microsoft's API for reading your tenant's admin announcements and service incidents. To access Graph, the portal needs credentials that prove it has permission to read that data.
Which option should I pick?
| Option | Best for | Pros | Cons |
|---|---|---|---|
| A — Managed identity | Portal hosted on Azure (App Service, Container Apps, VM) | No secrets to manage or rotate; most secure; the Azure platform handles tokens automatically | Only works when running on Azure infrastructure; requires CLI commands to grant Graph permissions (can't use the portal UI) |
| B — Automated script | Local development, or non-Azure servers | One command does everything; writes credentials to .env automatically; idempotent (safe to re-run) |
Requires PowerShell 7+ and Azure CLI installed locally; needs an admin account that can grant tenant-wide consent |
| C — Manual portal UI | When you can't run scripts, or prefer clicking through the Azure portal | No tools needed beyond a web browser; good for learning how app registrations work | More steps; easy to miss a step; you must copy/paste secrets carefully |
What is an "app registration"? It's an identity record in Microsoft Entra ID (formerly Azure AD) that represents this portal. Think of it as a username and password for the app itself (not for any human user). The portal uses it to call Microsoft Graph on behalf of your organization, without any user needing to sign in.
What is "admin consent"? Some Graph permissions are sensitive enough that a tenant administrator must explicitly approve them. The two permissions this portal needs (
ServiceMessage.Read.AllandServiceHealth.Read.All) both require admin consent — meaning a Global Administrator, Privileged Role Administrator, or Cloud Application Administrator must click "Grant" before the portal can read data.
What is a managed identity? When you host an app on Azure, the platform can automatically give it an identity (like a built-in service account) so it can call other Microsoft services without you having to create or store any passwords. Azure handles the credential lifecycle — no secrets to rotate or leak.
If the portal runs on Azure (App Service, Container Apps, or a VM), assign a
system- or user-assigned managed identity to the host and grant it two Graph
application permissions with admin consent. Then set one variable in .env:
USE_MANAGED_IDENTITY=true
System-assigned vs. user-assigned identity:
- System-assigned (simpler): Azure creates the identity automatically when you enable it on the App Service. It's tied to that specific resource and deleted if the resource is deleted. No extra configuration needed.
- User-assigned (more flexible): You create the identity separately and attach
it to one or more resources. Useful if you want to share one identity across
multiple apps. If you use this type, also add its client ID to
.env:AZURE_MI_CLIENT_ID=<managed-identity-client-id>
No client secret is needed — the platform issues the token automatically. On
App Service / Container Apps the IDENTITY_ENDPOINT and IDENTITY_HEADER
environment variables are set by the platform. On a VM the Azure IMDS endpoint
is used.
azd upusers: the post-provision hook runsscripts/create-entra-app.ps1which automatically grants these Graph permissions to the App Service managed identity. The Bicep deployment also assigns the identity the AzureReaderrole and configures the deployment subscription as the default Azure Service Health scope, so no separate Resource Health setup is needed.
Granting Graph permissions to a managed identity manually
Graph application permissions cannot be added through the portal UI for managed identities — you must use the CLI or Microsoft Graph API. Run these commands as a Global Administrator (or Privileged Role Administrator):
# 1. Get the managed identity's object ID
$miObjectId = az webapp identity show `
--name <app-service-name> -g <resource-group> `
--query principalId -o tsv
# 2. Get the Microsoft Graph service principal's object ID in your tenant
$graphSpId = az ad sp show `
--id "00000003-0000-0000-c000-000000000000" `
--query id -o tsv
# 3. Grant ServiceMessage.Read.All (Message Center)
az rest --method POST `
--uri "https://graph.microsoft.com/v1.0/servicePrincipals/$graphSpId/appRoleAssignments" `
--body "{`"principalId`":`"$miObjectId`",`"resourceId`":`"$graphSpId`",`"appRoleId`":`"1b620472-6534-4fe6-9df2-4680e8aa28ec`"}"
# 4. Grant ServiceHealth.Read.All (Service Health)
az rest --method POST `
--uri "https://graph.microsoft.com/v1.0/servicePrincipals/$graphSpId/appRoleAssignments" `
--body "{`"principalId`":`"$miObjectId`",`"resourceId`":`"$graphSpId`",`"appRoleId`":`"79c261e0-fe76-4144-aad5-bdc68fbe4037`"}"You can verify the assignments in Azure portal → Entra ID → Enterprise applications → search for the managed identity object ID → Permissions.
What this does: Runs a PowerShell script that creates the app registration,
adds the two required permissions, grants admin consent, creates a client secret,
and writes everything into your .env file automatically. If the app registration
already exists, it reuses it (safe to re-run).
You'll need:
- An account that can register apps and grant tenant-wide admin consent (Global Administrator, Privileged Role Administrator, or Cloud Application Administrator)
- Azure CLI installed
- PowerShell 7+
installed (
pwsh)
Steps:
-
Sign in to Azure CLI with your admin account:
az login --tenant <your-tenant-id>
(Replace
<your-tenant-id>with your organization's tenant ID — find it in Azure portal → Entra ID → Overview.) -
Run the setup script:
pwsh .\scripts\create-entra-app.ps1
-
The script writes
AZURE_TENANT_ID,AZURE_CLIENT_ID, andAZURE_CLIENT_SECRETinto.env. You're done — skip ahead to Step 3.
Optional overrides (set before running):
$env:APP_NAME = 'My Portal Name' # default: Microsoft Communications Portal
$env:SECRET_YEARS = '1' # default: 2 yearsUse this if you prefer clicking through the Azure portal or can't install PowerShell / Azure CLI.
Click to expand step-by-step instructions
Step 2a — Create the app registration
- Sign in to the Entra admin center (or Azure Portal → App registrations).
- Go to Identity → Applications → App registrations and click + New registration.
- Fill in:
- Name:
Microsoft Communications Portal(or any name you like) - Supported account types: Accounts in this organizational directory only (Single tenant) — this means only users and apps in your organization can use it.
- Redirect URI: leave blank — this app uses the "client-credentials flow" (app-to-app authentication), so no redirect is needed.
- Name:
- Click Register.
- On the Overview blade, copy these two values — you'll paste them into
.envlater:- Application (client) ID → this becomes
AZURE_CLIENT_ID - Directory (tenant) ID → this becomes
AZURE_TENANT_ID
- Application (client) ID → this becomes
Step 2b — Add Microsoft Graph API permissions
These permissions tell Microsoft Graph what data the portal is allowed to read.
-
In the new app registration, go to API permissions → + Add a permission.
-
Choose Microsoft Graph → Application permissions (NOT "Delegated" — that's for apps where a user signs in; this portal uses app-only access).
-
Search for and check each of these permissions:
ServiceMessage.Read.All— lets the portal read Message Center announcementsServiceHealth.Read.All— lets the portal read Service Health incidents
-
Click Add permissions.
-
Back on the API permissions blade, click Grant admin consent for <your tenant> and confirm. The Status column should show a green check ✅ for both permissions.
Don't see the "Grant admin consent" button? You need to be signed in as a Global Administrator, Privileged Role Administrator, or Cloud Application Administrator. Ask your IT admin to grant consent on your behalf.
Step 2c — Create a client secret
A client secret is like a password for the app registration. The portal uses it to prove its identity when calling Microsoft Graph.
- Go to Certificates & secrets → Client secrets → + New client secret.
- Enter a description (e.g.
portal-local-dev) and pick an expiration (6–24 months). Shorter is more secure but means you'll need to rotate it sooner. - Click Add.
- Immediately copy the secret's
Value(not the "Secret ID") — it's only shown once. This is yourAZURE_CLIENT_SECRET. If you navigate away before copying it, delete it and create a new one.
Step 2d — Create your .env file
- Copy the template:
# macOS / Linux cp .env.example .env # Windows PowerShell Copy-Item .env.example .env
- Open
.envin any text editor and paste in the three values you collected:AZURE_CLIENT_ID=00000000-0000-0000-0000-000000000000 AZURE_CLIENT_SECRET=your-client-secret-value AZURE_TENANT_ID=00000000-0000-0000-0000-000000000000
Keep secrets safe. Treat the client secret like a password — never commit
.envto source control (the included.gitignorealready excludes it). The server reads.envat startup, so restartnode server.jsafter any change.Data delay: Message Center and Service Health data can take up to ~1 hour to appear after first consent while Microsoft Graph provisions access to your tenant's data. This is a one-time delay.
(Optional — only needed for the Azure Service Health page. Skip if you don't need Azure-level health monitoring.)
What is Azure Service Health? While the M365 Service Health page (Step 2) shows incidents for Microsoft 365 services (Teams, Exchange, SharePoint, etc.), the Azure Service Health page shows incidents for Azure infrastructure services (Virtual Machines, App Service, Storage, etc.) in your specific subscriptions.
How it works: This page uses the
Azure Resource Manager (ARM) REST API
(management.azure.com) — a different API from Microsoft Graph. It reuses the same
credentials (managed identity or client secret) you set up in Step 2, but the
service principal also needs an Azure RBAC role (a permission assignment) on the
Azure subscription(s) you want to monitor.
What is RBAC? Role-Based Access Control is Azure's permission system. You assign a "role" (like "Reader") to an identity (like your app registration) on a "scope" (like a subscription). This grants the identity specific permissions within that scope.
Steps:
-
Assign a Reader role to your app registration's service principal (or managed identity) on each Azure subscription you want to monitor. Skip this for the subscription deployed by
azd up; its Bicep template creates the assignment:# Replace the placeholders with your values: az role assignment create \ --assignee <service-principal-app-id-or-managed-identity-object-id> \ --role "Reader" \ --scope "/subscriptions/<subscription-id>"
Which role?
Readergrants the management-plane read permissions used by the subscription picker and Resource Health endpoints. The specializedService Health Security Readerrole is only needed when the app must read sensitive security-advisory details.Where do I find my subscription ID? Azure portal → Subscriptions → click your subscription → the Subscription ID is shown on the Overview blade.
-
Set the default subscription in
.env(optional — you can also pick subscriptions in the UI):AZURE_SUBSCRIPTION_ID=your-subscription-id-here
Without a role assignment, the ARM API calls will return 403 Forbidden. The portal
includes a subscription picker on the Azure Service Health page so you can select
which subscriptions to monitor after the role is granted.
npm start
# or
node server.jsNavigate to http://localhost:3000. The home page shows status cards for all configured data sources so you can confirm which pages are working.
- Power Platform Release Planner — browse release features by product, wave, and date.
- Microsoft 365 Roadmap — current and upcoming M365 features from the official RSS feed.
- Azure Updates — Azure product announcements from the official RSS feed.
- Microsoft Fabric Roadmap — browse Fabric release features across 14 product areas from the official roadmap.
- Message Center — tenant-specific Microsoft 365 Message Center announcements, filterable by severity and date.
- Service Health — current service incidents and advisories for your tenant.
- Azure Service Health — Azure-level service health events, resource availability, and emerging issues for selected subscriptions.
- Product / service logos on every card — each card's product badge auto-resolves
to a Microsoft product icon from
/public/*.svgusing a curated alias map plus token-based fuzzy matching (Jaccard + coverage). Covers Power Platform, Dynamics 365, the full M365/Viva/Copilot family, security and identity (Defender, Entra, Purview, Intune), Office apps, and 30+ Azure services. Falls back silently when no icon matches, so unknown products never show a broken image. Implementation:static/product-icons.js. - ⬇ Export — one-click export of the currently filtered view to a self-contained
.htmlfile you can paste straight into a new Outlook email. The exporter (seestatic/outlook-export.js) commits to a single high-contrast light palette and puts inlinestyle="…"plus a redundant legacybgcolor="…"attribute on every cell so the table renders correctly even after Outlook's compose surface strips<style>blocks, CSS variables, and@mediarules. Outlook's built-in dark-mode rendering handles the dark conversion automatically. - 🛠 Generate Full Export — opens a modal preloaded with the page's current filters,
then lets you further focus the export by date range (the right date axis for the page —
GA Date, Published, Started, etc.) plus a checklist of products / services with
select-all, clear, and search. A live counter
shows how many items match before you generate. Items that don't carry a product /
service field appear under an
(Unclassified)bucket (pinned to the bottom of the list) so they can still be included or excluded explicitly. The output is the same Outlook-friendly HTML produced by the regular export. - Dark mode — toggle between light and dark themes from the header, or pass
?clawpilotTheme=light/?clawpilotTheme=darkon the URL. Theme is applied before first paint to avoid flash. - Auto-refresh — Graph-backed pages (Message Center, Service Health) refresh on an interval; non-Graph pages refresh on demand. Public roadmap refresh buttons bypass both browser and server TTL caches, while concurrent refreshes still coalesce into one upstream request.
- Shareable public filters — Microsoft 365 Roadmap, Azure Updates, and Fabric Roadmap keep search, sort, grouping, timeframe, and multi-select filters in the URL. Reloading or sharing the URL restores the same public-data view without persisting tenant-specific searches.
- Visible data freshness — public roadmap pages show the upstream source, fetch time, and whether results are fresh, cached, partial, stale, or unavailable. If a transient refresh fails, the server can serve bounded stale public data (24 hours for RSS/Fabric, 7 days for geography data) with an explicit warning instead of replacing useful results with an empty screen.
- Resilient tenant feeds — idempotent Microsoft Graph reads retry transient network, HTTP 429, and HTTP 5xx failures with bounded exponential backoff. If Graph remains unavailable, Message Center and Service Health can serve data cached within the previous 10 minutes with a visible stale-data warning.
- A per-feed "Top 5 most impactful changes this week" digest at the top of the page, covering the last 14 days and refreshed on demand.
- A Summarize with AI button inside every announcement's detail modal — produces a plain-language summary, impact rating (high / medium / low), audience (IT admins, end users, developers, security), and an admin-action flag with deadline.
- Highlights breaking changes, retirements, GA launches, security/compliance changes, and items requiring admin action. See AI summarization below.
The portal is built with defense-in-depth security suitable for enterprise environments. Every feature below is implemented and active by default — no additional configuration required unless noted. For the full security policy and vulnerability reporting instructions, see SECURITY.md.
The server refuses to start in a "fail-open" configuration. On every deployment
you must resolve to one of four explicit authentication modes, selected via
the AUTH_MODE environment variable (auto-inferred when it can be done safely):
AUTH_MODE |
When to use | What it enforces |
|---|---|---|
easyauth |
Azure App Service with Entra ID Easy Auth enabled. Auto-inferred when WEBSITE_INSTANCE_ID is present. |
Every /api/* request must carry a valid X-MS-CLIENT-PRINCIPAL header. The server base64-decodes the header, JSON-parses it, and requires auth_typ=aad plus an oid claim that matches the platform-injected X-MS-CLIENT-PRINCIPAL-ID header. Header presence alone is not sufficient. On App Service, protected requests are rejected with AUTH_NOT_ENFORCED unless the read-only platform signal WEBSITE_AUTH_ENABLED is True. |
reverse-proxy |
Docker / VM / on-prem behind an authenticating reverse proxy (nginx, Traefik, Azure Front Door, etc.) on a trusted network. | Every /api/* request must carry Authorization: Bearer <API_AUTH_TOKEN>. API_AUTH_TOKEN is required — the server refuses to start without it. Compared in constant time. This is defense-in-depth between the proxy and the app: even if the proxy misroutes an unauthenticated request, the token still gates access. |
none-loopback-only |
Local development on 127.0.0.1/::1. Auto-inferred when HOST is loopback. |
No token required. The server refuses to start under this mode if HOST is non-loopback. |
swa |
App Service used as a linked backend of an Azure Static Web App (see Quick Deploy option 2). Never auto-inferred — it must be set explicitly. | Every /api/* request must carry an X-MS-CLIENT-PRINCIPAL header holding either a Static Web Apps client principal (userId + userDetails, with authenticated present in userRoles) or an App Service Easy Auth principal. Both shapes are accepted because the linked-backend hop may re-encode the header; anything else is rejected. This is safe only because linking auto-configures the Azure Static Web Apps (Linked) identity provider, which blocks any request that did not arrive through the Static Web App. |
Fail-fast behavior. The server calls process.exit(1) at startup on any of
these misconfigurations:
AUTH_MODEis unset ANDHOSTis non-loopback ANDWEBSITE_INSTANCE_IDis unset (no safe inference possible).AUTH_MODE=none-loopback-onlywith a non-loopbackHOST.AUTH_MODE=reverse-proxywith noAPI_AUTH_TOKEN.AUTH_MODEis set to any value other than the four listed above.- Binding to a non-loopback host without
ALLOW_REMOTE_BIND=true(defense in depth against accidental exposure).
AUTH_MODE=swa additionally logs a warning when it is used outside App Service
(WEBSITE_INSTANCE_ID unset) or when WEBSITE_AUTH_ENABLED is not True,
since both suggest the linked-backend identity provider is missing.
Migration from earlier versions. Older builds accepted a HOST=0.0.0.0
container as long as API_AUTH_TOKEN was set. That still works, but you must
now also set AUTH_MODE=reverse-proxy explicitly (or easyauth on App
Service). The old header-presence Easy Auth check has been replaced with a
proper cryptographic-style header validator: spoofed values like
X-MS-CLIENT-PRINCIPAL: x no longer bypass the guard.
| Feature | What it does | What it prevents |
|---|---|---|
| Entra ID Easy Auth | Set the optional authClientId Bicep parameter to require organizational sign-in before any request reaches the App Service. Without it, public feeds remain available while tenant and AI API routes fail closed in the application. See Quick Deploy. |
Unauthorized access to tenant-sensitive Graph data, Azure Resource Health endpoints, and billed LLM API calls. |
| Managed identity support | Preferred for Azure deployments (USE_MANAGED_IDENTITY=true). Azure issues and rotates tokens automatically — no client secret stored anywhere. See Setup → Option A. |
Credential leakage, secret sprawl, and manual rotation failures. |
| Admin endpoint auth | The /api/empty-products endpoint is restricted to loopback addresses (127.0.0.1, ::1). When ADMIN_TOKEN is set, a bearer token is additionally required for both reads and mutations, compared using crypto.timingSafeEqual. |
Unauthorized cache manipulation and timing side-channel attacks on token comparison. |
| Graph token isolation | OAuth bearer tokens for Microsoft Graph are never sent to any host other than graph.microsoft.com. If an @odata.nextLink points at a different host, the request is rejected. |
Token exfiltration via open-redirect or DNS rebinding on upstream APIs. |
| Feature | What it does | What it prevents |
|---|---|---|
| Strict Content Security Policy (CSP) with nonce | Every HTML response includes a CSP header. Inline <script> tags are authorized via a per-request cryptographic nonce (script-src 'self' 'nonce-…'); 'unsafe-inline' is not used. Additional directives: frame-ancestors 'none', base-uri 'none', form-action 'none', object-src 'none', frame-src 'none', media-src 'none', worker-src 'none'. |
Injected scripts, clickjacking (frame-ancestors), base-tag hijacking (base-uri), form-action redirection, and plugin/embed-based attacks. |
| Allow-list HTML sanitizer | A single hardened sanitizer in static/util.js is shared across all pages. It parses untrusted feed / Graph HTML via DOMParser (which does not execute scripts), keeps only an explicit allow-list of safe tags and attributes (links forced to target="_blank" rel="noopener noreferrer"), and drops everything else — including <script>, <style>, <iframe>, <math>, <template>, <svg>, <noscript>, event-handler attributes, javascript: / data: / blob: URLs, and style attributes. |
Stored XSS and mutation-XSS from upstream Microsoft feed data and Graph API responses. The allow-list approach is far more resilient than deny-lists. |
| Event delegation (no inline handlers) | All user-facing event handlers use a data-act delegation system in static/util.js. No onclick=, onchange=, or other inline handler attributes appear anywhere in the markup. |
Inline-handler injection that would bypass CSP. Ensures the nonce-based CSP policy is fully enforceable with no exceptions. |
| Feature | What it does | What it prevents |
|---|---|---|
| Redirect-following restricted to Microsoft hosts | The RSS and release-plan fetchers only follow HTTP redirects to *.microsoft.com and *.azure.com over HTTPS, up to a maximum of 5 hops. Redirects to any other host are rejected with a logged error. |
SSRF attacks where a compromised or malicious upstream redirects the server to internal network resources or attacker-controlled hosts. |
| UUID validation on inputs | Product IDs and other identifiers passed via query parameters are validated as well-formed UUIDs before being interpolated into upstream API paths. | Path injection and cache-key abuse through malformed identifiers. |
| Feature | What it does | What it prevents |
|---|---|---|
| Prototype pollution guard | The JSON body parser uses a reviver function that strips __proto__, constructor, and prototype keys before they reach application code. |
Prototype pollution attacks that could modify object behavior, bypass validation, or escalate to remote code execution. |
| Path traversal protection | The /static/ and /public/ file-serving routes reject .., \, and any resolved path that escapes the intended root directory using path.normalize() checks. |
Directory traversal attacks that could read arbitrary files from the server filesystem (e.g., .env, /etc/passwd). |
| 1 MB request body limit | JSON POST bodies are capped at 1 MB (/api/summarize uses a tighter 256 KB cap). Oversized payloads stop buffering and receive HTTP 413. |
Memory exhaustion and denial-of-service via large request bodies. |
| Feature | What it does | What it prevents |
|---|---|---|
| Per-IP rate limiting on every endpoint | All API endpoints enforce per-IP fixed-window rate limits. Set TRUST_PROXY=true when running behind a reverse proxy so the limiter reads the client IP correctly (Azure App Service X-Azure-ClientIP header preferred, then the last X-Forwarded-For hop — not the first, which is client-spoofable). |
Denial-of-service, brute-force abuse, and billed API cost runaway. |
| Rate limit tiers | AI summarize: 5/min. AI digest: 10/min. Graph + RSS endpoints: 60/min. Proxy: 600/min. Subscription endpoints: 30/min. Exceeded requests receive HTTP 429 with a Retry-After header. |
Proportional protection — tighter limits on expensive AI calls, looser on read-only feeds. |
| Daily LLM budget cap | A global daily limit (default 200 calls/day, configurable via LLM_DAILY_LIMIT) caps total LLM invocations per UTC day regardless of how many clients are hitting the endpoint. |
Runaway AI API spend from automated abuse, misconfigured clients, or compromised deployments. |
| Cache size caps | The upstream response cache and rate-limit bucket map are both hard-capped (500 and 10,000 entries respectively). Expired entries are evicted first, then oldest-inserted. | Memory exhaustion from clients spraying unique cache keys or URLs to grow internal maps without bound. |
| Feature | What it does | What it prevents |
|---|---|---|
| Loopback-only binding by default | The server binds to 127.0.0.1 and refuses to start on a non-loopback host unless ALLOW_REMOTE_BIND=true AND a suitable AUTH_MODE are explicitly set. When overridden, a prominent warning is logged listing the specific risks (Graph tokens, billed LLM APIs, admin endpoints) and the resolved AUTH_MODE. See Authentication modes for the full matrix. |
Accidental exposure of credential-bearing services to untrusted networks. |
| HSTS (HTTP Strict Transport Security) | All JSON and HTML responses include Strict-Transport-Security: max-age=31536000; includeSubDomains. |
Protocol downgrade attacks and SSL-stripping when deployed behind a TLS-terminating reverse proxy. |
| CORS allow-list | Cross-origin requests are only permitted for origins explicitly listed in the CORS_ORIGINS environment variable (comma-separated). If unset, no Access-Control-Allow-Origin header is emitted (same-origin only). Wildcard (*) is not supported. |
Cross-site data theft from unauthorized origins. |
| Upstream request timeouts | All outbound HTTP requests enforce timeouts: 15 seconds for Graph/RSS/proxy calls, 30 seconds for AI calls. | Hung sockets and resource exhaustion from slow or unresponsive upstreams. |
| Keep-alive connection pooling | A shared https.Agent with keepAlive: true and maxSockets: 32 reuses TLS connections to upstream hosts. |
Socket exhaustion and excessive TLS handshake overhead under load. |
| Secure session cookies | Session cookies are HttpOnly, SameSite=Strict, and Secure for network-facing or TLS requests. The decision uses the socket and trusted proxy protocol, never the client-controlled Host header. |
Session disclosure over plaintext transport or cookie-flag downgrades through a spoofed host header. |
All responses include defense-in-depth headers:
| Header | Value | What it prevents |
|---|---|---|
X-Content-Type-Options |
nosniff |
MIME-type sniffing that could reinterpret a response as executable script. |
X-Frame-Options |
DENY |
Clickjacking via <iframe> embedding (defense-in-depth alongside CSP frame-ancestors). |
Referrer-Policy |
no-referrer |
Leaking sensitive URL parameters (e.g., tokens, subscription IDs) to external sites via the Referer header. |
Cache-Control |
no-store (HTML) |
Prevents caching of HTML pages that may contain nonces or tenant-specific data. |
Content-Security-Policy |
Full policy (see above) | Comprehensive XSS, clickjacking, and resource-loading restrictions. |
Public feed JSON over 4 KB uses the shared asynchronous gzip/deflate response
path. Responses include X-Cache, X-Data-Fetched-At, and a meta object so
clients can distinguish fresh, cache-hit, coalesced, and stale results. CORS
responses vary on both Origin and Accept-Encoding.
| Feature | What it does | What it prevents |
|---|---|---|
| Prompt injection defenses | Both the summarize and digest system prompts include explicit instructions to ignore any directives, prompts, or instructions embedded in feed item titles or descriptions. The LLM is instructed to only analyze factual content and never change its output format, role, or behavior based on item content. | Indirect prompt injection via malicious content in upstream Microsoft feeds (e.g., a crafted Azure Update title designed to hijack the LLM). |
| Strict JSON output format | All LLM calls request structured JSON responses with a fixed schema. Responses are parsed and validated before being returned to clients. | Output manipulation that could inject malicious content into the UI through LLM responses. |
| AI provider auto-detection | The server detects the AI provider in priority order (Azure OpenAI → OpenAI → GitHub Models). When no provider is configured, AI features are cleanly disabled — /api/summarize returns 503, and the UI silently omits AI panels. |
Misconfiguration errors and confused failure modes when AI is not set up. |
| Feature | What it does | What it prevents |
|---|---|---|
.env excluded from source control |
The .gitignore file excludes .env and other secret-bearing files. |
Accidental commit of API keys, client secrets, or tokens to a public or shared repository. |
| No secrets in code | All credentials are loaded exclusively from environment variables or Azure managed identity. No secrets are hardcoded. | Credential exposure through source code inspection or repository access. |
| Client secret rotation guidance | The setup documentation includes expiration guidance and rotation instructions for Entra ID client secrets. | Expired or compromised credentials going unnoticed. |
| Feature | What it does | What it prevents |
|---|---|---|
| Single runtime dependency | The server depends only on dotenv. All HTTP, HTTPS, crypto, compression, and file-serving functionality uses Node.js built-in modules. |
Supply chain attacks via compromised npm packages. A minimal dependency tree means a minimal attack surface. |
| No native add-ons | The server uses no native C/C++ modules — pure JavaScript only. | Build-time supply chain attacks and binary compatibility issues across platforms. |
| Node.js 24 LTS | The engines field in package.json enforces Node.js 24+, ensuring the latest security patches and built-in API improvements. |
Running on outdated runtimes with known vulnerabilities. |
The portal can call an LLM (Large Language Model) to surface what actually matters in the firehose of Microsoft updates. When enabled, seven supported feed pages show:
- A Top 5 most impactful changes digest at the top of the page (covers the last 14 days, refreshed on demand), with one-line themes and an overall headline.
- A ✨ Summarize with AI button inside each announcement's detail modal — produces a plain-language summary, an impact rating (high/medium/low), the intended audience (IT admins, end users, developers, security), and a flag if admin action is required before a deadline.
How to enable: Pick one of the three providers below, set the corresponding
variables in .env, and restart the server. The server auto-detects which provider
to use (priority: Azure OpenAI → OpenAI → GitHub Models). When no AI provider is
configured the portal still works fine — the AI panel just shows a note pointing
to .env.example.
Which provider should I pick?
| Provider | Env vars to set | Pros | Cons | Cost |
|---|---|---|---|---|
| Azure OpenAI | AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_API_KEY, AZURE_OPENAI_DEPLOYMENT, (optional) AZURE_OPENAI_API_VERSION |
Azure-managed data privacy controls; enterprise SLAs; content filtering built in | Requires an Azure OpenAI resource and organizational review of the selected region/model; most setup steps | Pay-per-token (very low with gpt-4o-mini) |
| OpenAI | OPENAI_API_KEY, (optional) OPENAI_MODEL, OPENAI_BASE_URL |
Fastest setup — just paste an API key; no Azure subscription needed | Data leaves your tenant; usage is billed to your OpenAI account | Pay-per-token |
| GitHub Models | GITHUB_TOKEN, (optional) GITHUB_MODEL |
Free tier — great for trying it out; no billing setup | Rate limits on free tier; data processed by GitHub; not for production workloads | Free (with limits) |
Recommended model: gpt-4o-mini (or your deployment name for Azure OpenAI) —
fast, cheap, and plenty capable for this kind of triage.
Example .env for each provider:
# ── Azure OpenAI (recommended for enterprise) ──
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com
AZURE_OPENAI_API_KEY=your-azure-openai-key
AZURE_OPENAI_DEPLOYMENT=gpt-4o-mini
# ── OpenAI (quickest setup) ──
OPENAI_API_KEY=sk-...
# ── GitHub Models (free, great for testing) ──
GITHUB_TOKEN=ghp_...Responses are cached in memory (summaries for 10 minutes, digests for 15 minutes)
so repeat views don't burn tokens. A per-process daily call budget defaults to 200
and can be lowered with LLM_DAILY_LIMIT. Feed content, including tenant-specific
Message Center or Service Health text, is sent to the provider you configure; use
only a provider and region approved for that data.
The Node server exposes the following local endpoints (all return JSON):
| Endpoint | Description | Auth | Rate limit |
|---|---|---|---|
GET /healthz or /health |
Health check / liveness probe | None | — |
GET /readyz |
Readiness probe; returns HTTP 503 while the process is shutting down | None | — |
GET /api/auth-check |
Reports auth configuration status for Graph, ARM, and AI | None | — |
GET /proxy?productId=...&langCode=... |
Power Platform Release Planner proxy (follows 301/302/307/308 redirects; auto-skips IDs cached as known-empty). Also reachable as GET /api/proxy?..., which is the path the pages call so Azure Static Web Apps can forward it to the linked backend. |
None | 600/min |
GET /api/m365updates[?refresh=1] |
Microsoft 365 Roadmap RSS, parsed to JSON; optional forced refresh | None | 60/min |
GET /api/azureupdates[?refresh=1] |
Azure Updates RSS, parsed to JSON; optional forced refresh | None | 60/min |
GET /api/fabricroadmap[?refresh=1] |
Microsoft Fabric Roadmap JSON (14 product areas); optional forced refresh | None | 60/min |
GET /api/featuregeo[?refresh=1] |
Regional release plans (feature availability by geography), normalised from the public release plans site (30 min server cache) | None | 60/min |
GET /api/messagecenter |
Microsoft 365 Message Center via Microsoft Graph | .env |
60/min |
GET /api/servicehealth |
Microsoft 365 Service Health via Microsoft Graph | .env |
60/min |
GET /api/subscriptions |
List Azure subscriptions (for subscription picker) | .env |
30/min |
GET /api/subscriptions/selected |
Get currently selected Azure subscriptions | None | 30/min |
POST /api/subscriptions/selected |
Save selected Azure subscriptions | None | 30/min |
GET /api/azure-resource-health/emerging-issues |
Azure emerging issues (tenant-level) | .env |
60/min |
GET /api/azure-resource-health/events |
Azure Resource Health events (subscription-scoped) | .env |
60/min |
GET /api/azure-resource-health/availability-statuses |
Azure resource availability summary | .env |
60/min |
GET /api/azure-resource-health/impacted-resources |
Resources impacted by an event | .env |
60/min |
GET /api/azure-resource-health/resource-events |
Events for a specific resource | .env |
60/min |
GET /api/azure-resource-health/resource-availability |
Availability history for a resource | .env |
60/min |
GET /api/azure-resource-health/resource-status |
Current status of a resource | .env |
60/min |
GET /api/ai-status |
Reports whether AI is configured and which provider is active | None | — |
POST /api/summarize |
Body {source, items[]} → per-item AI summaries |
AI provider | 5/min |
GET /api/impact-digest?source=azure|m365|messagecenter|servicehealth|fabricroadmap&limit=5&windowDays=14 |
Top N most impactful items for a source | AI provider | 10/min |
GET /api/empty-products |
List product IDs cached as known-empty by the /proxy route |
Loopback + ADMIN_TOKEN |
— |
DELETE /api/empty-products |
Clear the entire known-empty cache | Loopback + ADMIN_TOKEN |
— |
GET /static/<file> |
Shared client JS (util.js, nav.js, product-icons.js, outlook-export.js, ai-insights.js, export-formats.js, subscription-picker.js) and CSS (common.css, page-specific stylesheets) |
None | — |
GET /public/<file> |
Microsoft product / service SVG icons | None | — |
OAuth tokens for Microsoft Graph are cached in-memory and refreshed 60 seconds before expiry.
Microsoft Graph feed reads retry transient failures up to three attempts and can
serve successful responses cached within the previous 10 minutes when Graph is
temporarily unavailable.
Rate limits are per-IP fixed-window counters; set TRUST_PROXY=true only behind a
trusted reverse proxy. The server prefers App Service's X-Azure-ClientIP, then the
last X-Forwarded-For hop appended by the proxy.
AI provider responses are cached in-memory (summarize: 10 min, digest: 15 min, hashed by input).
The known-empty product cache is persisted to empty-products.json so
restarts don't lose the auto-skip list.
Versioned /static/ URLs use a 24-hour browser cache; unversioned development
URLs continue to revalidate with ETags. Static /public/ icons are
cached in memory, sent with a 24-hour Cache-Control and an ETag, and SVGs over
4 KB are pre-compressed once per process.
On SIGTERM or SIGINT, the server marks /readyz unavailable, stops accepting
new connections after a short readiness propagation window, drains active
requests, and closes upstream keep-alive sockets. Set SHUTDOWN_GRACE_MS
(0–30,000; default 5,000) to control that window. The included container health
checks poll /readyz every two seconds so they can observe the transition.
Set SHUTDOWN_TIMEOUT_MS (1,000–30,000; default 10,000) to control the forced
shutdown deadline.
home.html Home / dashboard landing page
powerplatform.html Power Platform Release Planner UI
featuregeo.html Regional Release Plans (world map + rollout table by geography)
m365updates.html M365 Roadmap UI
azureupdates.html Azure Updates UI
fabricroadmap.html Microsoft Fabric Roadmap UI
messagecenter.html M365 Message Center UI
servicehealth.html M365 Service Health UI
azureservicehealth.html Azure Service Health UI
guidedreport.html Guided Report wizard (multi-source report builder)
server.js Node HTTP server, static file host, API proxy, AI endpoints
static/
util.js Shared client-side utilities (escapeHtml, sanitizeHtml, safeUrl, theme toggle, event delegation)
nav.js Shared navigation header and dropdown menu (injected into every page)
common.css Shared CSS (nav dropdowns, header, layout primitives)
ai-insights.js Shared client-side AI helper (digest panel + per-item summarize)
product-icons.js Shared client-side product-icon resolver (alias map + fuzzy matcher)
outlook-export.js Shared client-side Outlook-friendly HTML exporter (inline styles + bgcolor)
export-formats.js Multi-format export (HTML, Markdown, PDF, Word) for the Generate Full Export modal
subscription-picker.js Shared Azure subscription selection picker
featuregeo.css Regional Release Plans page styles (map, filter rail, rollout table)
worldmap.js Generated world outline (Natural Earth 110m) used by the Regional Release Plans map
public/ Microsoft product / service SVG icons served at /public/<file>.svg
empty-products.json Persisted cache of known-empty Release Planner product IDs (auto-skip list)
package.json Dependencies (dotenv only)
.env.example Template for Graph auth, AI providers, and server/security options
scripts/capture-screenshots.js Playwright script that regenerates the README screenshot gallery
scripts/build-worldmap.js Regenerates static/worldmap.js from Natural Earth 110m TopoJSON
scripts/build-swa.js Builds dist/ for Azure Static Web Apps (stages pages + assets, computes CSP script hashes)
staticwebapp.config.json Azure Static Web Apps routes, auth, security headers (source template — placeholders filled by build-swa.js)
dist/ Generated Static Web Apps payload (git-ignored; created by npm run build:swa)
screenshots/ Light + dark mode PNGs rendered into the README above
- Node.js 24 LTS or later (uses the built-in
http,https,crypto, and globalURLAPIs — no native add-ons) - npm (bundled with Node.js)
- An Entra app registration (only for the Message Center and Service Health pages)
- PowerShell 7+ and Azure CLI (only if using
scripts/create-entra-app.ps1)
This project welcomes suggestions and feedback. Please open an issue to discuss changes before submitting a pull request. See CODE_OF_CONDUCT.md for community guidelines.
This project is licensed under the MIT License — see LICENSE for details.
Questions, feedback, or ideas to make this portal better? Reach out to Russ Rimmerman at russ.rimmerman@microsoft.com or connect on LinkedIn.

















