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) |
| 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.
Six of nine 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 all 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 (recommended for shared deployments):
By default the App Service is deployed without login requirements — anyone with the
URL can access it. To require users to sign in with their organizational account
before using the portal, set the authClientId parameter:
azd env set AUTH_CLIENT_ID <your-entra-app-client-id>
azd upThis turns on "Easy Auth" on the App Service, so unauthenticated visitors are redirected to the Entra ID login page. Strongly recommended if the portal will hold Graph credentials or AI API keys, since those enable access to tenant data and billed API calls.
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. Six 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. Using-p 3000:3000instead exposes it on all network interfaces — only do that behind an authenticated reverse proxy on a trusted network, since the app can hold Graph credentials and call billed LLM APIs.
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. Six of nine 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.
All 9 pages are shown in light and dark mode below (18 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 and Service Health pages. Skip this if you only need the other six 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 — no manual steps 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:
# 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?
Readeris the simplest choice — it grants read-only access to all resources in the subscription. For a more restrictive option, useResource Health Readerwhich only grants access to health data. Either works.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 that lets you build a focused 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.
- 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.
| Feature | What it does | What it prevents |
|---|---|---|
| Entra ID Easy Auth | The Bicep infrastructure supports enabling Entra ID authentication on the App Service via the authClientId parameter. When set, all requests require an organizational sign-in before reaching the app. 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 (e.g., /api/summarize) are capped at 1 MB. Oversized payloads destroy the socket immediately. |
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 is explicitly set. When overridden, a prominent warning is logged listing the specific risks (Graph tokens, billed LLM APIs, admin endpoints). |
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. |
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. |
| 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, every feed page shows:
- 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 |
Data stays in your Azure tenant (best for compliance); enterprise SLAs; content filtering built in | Requires an Azure OpenAI resource (need to apply for access if you don't have one); 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.
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 /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) | None | 600/min |
GET /api/m365updates |
Microsoft 365 Roadmap RSS, parsed to JSON | None | 60/min |
GET /api/azureupdates |
Azure Updates RSS, parsed to JSON | None | 60/min |
GET /api/fabricroadmap |
Microsoft Fabric Roadmap JSON (14 product areas) | 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.
Rate limits are per-IP fixed-window counters; set TRUST_PROXY=true when running behind a reverse proxy so the limiter reads the client IP from X-Forwarded-For.
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.
Static /public/ icons are sent with a 24-hour Cache-Control and an ETag, and gzipped when >1 KB.
home.html Home / dashboard landing page
powerplatform.html Power Platform Release Planner UI
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
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
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.

















