← All entries

Monitoring Power Pages traffic + detecting attacks + data-exfiltration coverage

Praxisis D365 Wiki · Public reference · Client-specific details redacted

Source: Claude.ai chat (original: monitoring-website-traffic-and-detecting-attacks-on-power-pa__d726b3b6.md) Added: 2026-10-02

Summary

  • WAF/traffic logs only see HTTP shape. Data exfiltration through APIs looks like perfectly well-formed authenticated traffic — need a different set of signals.
  • Four-step monitoring design: (1) enumerate the channels before monitoring anything, (2) baseline what's actually exposed, (3) stand up three monitoring feeds (WAF + Dataverse activity + Entra sign-ins), (4) control not just observe.

Key takeaways

Step 1 — Channels (more egress paths than people expect):

  • Portals Web API /_api/<entityset> → full CRUD. Follows table + column permissions via web role. Needs CSRF token.
  • OData feeds /_odata → read-only, list-based. Deprecated — Microsoft directs you to the Web API.
  • Lists / forms / Liquid → page-rendered, no separate endpoint to watch.
  • Web files + annotations /file.axd, notes → often overlooked.
  • Direct Dataverse Web API *.crm3.dynamics.com/api/data/v9.2 → bypasses the portal entirely (app registrations, S2S). No amount of WAF tuning sees this.
  • Flows / connectors / Synapse Link → Dataverse-side egress, also outside Power Pages observability.

Step 2 — Baseline (two drift-detection checks):

  • Anonymous OData exposure: browse to {PortalUrl}/_odata in InPrivate without auth. If a feed renders instead of returning 403, you have an anonymous data leak (the classic UpGuard/Power Apps Portals class). Design studio now blocks saving OData-enabled lists with permissions disabled — but the guard isn't API-enforced; a direct PATCH to adx_entitylists({list-id}) via the Dataverse Web API can override it. Check adx_entitylist` records directly, don't trust the UI.
  • Web API surface: GET adx_sitesettings?$filter=contains(adx_name,'Webapi'). Flag anything where fields is * rather than an explicit list. Cross-reference against table permissions on the Anonymous Users web role. Enhanced data model sites use mspp_sitesetting instead of adx_sitesettings.

Step 3 — Three monitoring feeds:

  • A. WAF logs for portal API traffic — the only per-request view of /_api/ + /_odata calls with client IP. Useful alert shapes: high-volume anonymous /_api/ GETs from one IP (enumeration); sustained 403s in sequence (probing table permissions); any /_odata traffic (if deprecated); large $top with no $filter (bulk pull vs. app usage).
  • B. Dataverse activity logging — WAF tells you a request happened; Dataverse logs tell you which records left. Enable auditing at env level + Single-record (Retrieve) + Multi-record (RetrieveMultiple) auditing on PII tables. Gotchas: option appears only once minimum Office licensing is met · Purview 3-KB limit per audit record means one Dataverse record can split into multiple Office records — use CorrelationId to reassemble.
  • C. Entra sign-in logs for the non-portal path — filter SigninLogs (and the service-principal sign-in category) for the Dataverse resource. Catches app-registration / leftover S2S exfiltration. Alert on any new service principal authenticating to Dataverse, and on non-interactive sign-in volume outside normal integration windows. Pairs with the offboarding audit — a departed consultant's app registration is the classic finding.

Step 4 — Controls (not just observation):

  • Rate-limit the API paths: WAF custom rule matching request URI containing /_api/ with a rate-limit action. Cheapest single control.
  • Column permissions: narrow the fields site setting to columns the front end actually reads. Reduces blast radius and makes anomalous $select values visible.
  • CORS: audit environment-level + site-level allowed origins. Over-broad allowed-origins = third-party page can invoke your Web API inside an authenticated user's browser session.
  • Retire OData feeds entirely — deprecated anyway; disabling removes a whole surface.
  • Site Checker in release pipeline — run after every prod import and diff output. Catches unsecured-list regression before an attacker does.

Learnings / how to apply

  • OData feeds are read-only; the Web API allows full CRUD. The alerting priority is asymmetric: anonymous write attempts against /_api/ deserve a page; anonymous reads deserve a daily digest.
  • None of this is a SIEM. If a client needs real correlation across WAF + identity + Dataverse read events with >90-day retention, the answer is shipping all three feeds into Sentinel or an existing SIEM. Scope that as a separate conversation before building bespoke Power Automate glue.
  • Package this as a reusable Praxisis "Power Pages Security Monitoring" document template (navy/blue, Arial, banded tables, <<placeholder>> tokens on cover) — reusable across clients. Sections: 5 monitoring layers + alerting matrix + hardening + implementation checklist with Owner/Status columns.
  • Thresholds in any alerting matrix are starting points that need tuning against a week or two of baseline traffic before notifications turn on.

Need this applied to your own tenant?

Praxisis Sustain Self-Serve builds a private AI Wiki trained on your documents and tickets, with free Tier-2 support when the AI cannot answer. Book a 30-min overview.