DEV Community

Cover image for Building a Live Azure Exposure Dashboard with Zero npm Dependencies
Vignesh Athiappan
Vignesh Athiappan

Posted on

Building a Live Azure Exposure Dashboard with Zero npm Dependencies

Every cloud team eventually needs a quick answer to one question: is our Azure estate exposed or broken right now, and how badly?

Usually that answer comes from a mix of Defender for Cloud alerts, tribal knowledge, and a few az commands run during an incident. I wanted something continuous, so I designed a read-only dashboard. It reads Azure configuration on a timer, probes live endpoints from the internet, checks everything against rules, scores it, and streams the result to the browser.

Here's how it works and the design decisions behind it.

Start with one request path

A typical locked-down web workload on Azure looks like this:

User → Front Door (WAF) → Private Link → App Service → VNet integration → SQL Managed Instance
Enter fullscreen mode Exit fullscreen mode

Tracing that path by hand with az commands is slow. It also shows how easy it is for one link in the chain to quietly drift. The usual misconfigurations in a setup like this are:

  • A WAF in Detection mode, which logs attacks but doesn't block them
  • Apps still reachable directly on *.azurewebsites.net, bypassing Front Door and the WAF
  • VNet peerings or private endpoints in a disconnected state
  • PaaS services with public network access still enabled

None of these cause errors. Everything keeps working, and that's what makes them dangerous. So the goal is to automate the trace for every app and resource, re-check it continuously, and make any gap visible.

What it checks

Area What is checked
App Services (Web, Functions, Logic App Standard) The full chain per app: custom domain, TLS, HTTPS redirect, WAF attached and in Prevention mode, health probe, Private Link approved, public access locked, SCM/Kudu restricted, FTP disabled, managed identity, VNet integration with route-all, and NSG reachability to SQL MI on 1433 and 11000–11999
Live probes (every 20s) Custom domain through Front Door must answer. The direct *.azurewebsites.net URL must be blocked. DNS CNAME must point to azurefd.net. SQL MI hostname must resolve to a private IP
PaaS resources Storage, API Management, Logic Apps, Redis, Azure OpenAI / AI Services, AI Search, Service Bus, Key Vault, Cosmos DB, Azure SQL, SQL MI. Checks cover public access, private endpoints, TLS, local/key auth, soft delete, purge protection
Network NSG rules open to the internet, subnets without an NSG, peerings not Connected, private endpoints not Approved, private DNS zones not linked to any VNet

The live probes are the part I'd recommend to anyone building something like this. Configuration tells you what should happen; probes tell you what does happen. An app can look locked down in ARM and still answer on its default hostname.

Architecture

┌──────────── Browser (Angular 21) ────────────┐
│ Dashboard · Apps · Resources · Network ·     │
│ Topology · Admin · Preferences               │
└───────────────┬──────────────────────────────┘
                │ HTTPS + Server-Sent Events
┌───────────────▼──────────────────────────────┐
│ Server (Node.js, zero npm dependencies)      │
│  auth      → Entra ID SSO + sessions         │
│  collect   → ARM snapshot (GET only)         │
│  checks    → rules & scoring                 │
│  probes    → live HTTPS / DNS                │
│  store     → JSON files                      │
└──────┬─────────────────────┬─────────────────┘
       │ read-only GETs      │ GET / DNS lookups
       ▼                     ▼
 Azure Resource Manager   Public internet
Enter fullscreen mode Exit fullscreen mode

The server has no npm dependencies. It uses only Node's built-in http, crypto, dns and fetch. For a security tool, keeping the supply-chain surface small matters.

The loop

  1. Scan (every 120s): Collect VNets, NSGs, private endpoints, private DNS zones, sites and their config, SQL MI, Front Door (routes, origins, custom domains, WAF policies) and every PaaS type across the selected subscriptions. GET requests only. It never reads secrets or app settings.
  2. Probe (every 20s): Real HTTPS and DNS calls, max 8 concurrent, keeping the last 40 results per endpoint for latency charts.
  3. Evaluate: Turn the snapshot and probe results into pass / warn / fail checks, then compute scores.
  4. Stream: Push the new state to every open browser over Server-Sent Events. No polling and no WebSocket library.
  5. Remember: Store one estate-score point per minute for the trend line.

Adding a new PaaS resource type is a single entry in a checks table, which keeps it cheap to grow coverage.

Code layout

server/
├── azure.mjs       ARM client: token, paging, throttling, subscription list
├── collect.mjs     multi-subscription snapshot
├── resources.mjs   per-resource-type checks (add a type = one entry)
├── catalog.mjs     check catalog, default rules, rule validation
├── checks.mjs      app chain model, network findings, scoring, topology
├── probes.mjs      live HTTPS / DNS probes
├── auth.mjs        Entra ID sign-in, session cookies, roles
├── store.mjs       JSON persistence + settings validation
└── index.mjs       HTTP server, routes, SSE, timers
web/                Angular 21 (standalone, signals, zoneless, lazy routes)
data/               git-ignored: settings, rules, history, auth config
Enter fullscreen mode Exit fullscreen mode

Scoring that can't be gamed

A naive pass-rate score lies. Ninety passing low-severity checks will hide one wide-open database. So the scoring works like this:

  1. Every check has a severity weight: critical 10, high 6, medium 3, low 1.
  2. A pass earns the full weight, a warn earns 50%, and a fail earns 0.
  3. Score = earned ÷ possible × 100, then capped:
    • at most 49 if any critical check fails
    • at most 79 if any high check fails
  4. Category score = average across the items of that type.
  5. Estate score = average of the category scores, so hundreds of Logic Apps can't drown out a couple of API Management instances.
  6. Grades: A ≥ 90, B ≥ 75, C ≥ 60, D ≥ 40, F below that.

The caps are the key design decision. One serious gap should pull the score down hard, and no amount of passing hygiene checks should make up for it. Admins can tune weights, caps and thresholds in the UI, and scores recalculate instantly without a rescan.

SSO without a client secret

Sign-in goes through Microsoft Entra ID with an app registration configured as a single-page application, so there's no client secret to manage.

The flow:

  1. The server redirects to Entra with state, nonce and a PKCE challenge, and sets a short-lived signed "flow" cookie.
  2. After sign-in, Entra redirects back with an authorization code.
  3. The browser redeems the code, because Entra only allows browser redemption for SPA redirect URIs.
  4. The browser posts the ID token to the server, which verifies the signature (JWKS), issuer, audience, tenant, expiry, nonce and state.
  5. The server sets an HMAC-signed, HttpOnly, SameSite=Lax session cookie that lasts 8 hours and is Secure over HTTPS.

Other hardening:

  • Every page and API route requires a session, apart from the sign-in routes
  • Cross-origin writes are blocked
  • returnTo links can only point inside the app, so it can't be used as an open redirect
  • If a client secret is ever configured, the server switches to the classic server-side flow automatically

A design point worth stressing: if an app registration doesn't require user assignment, anyone in the tenant can sign in. For a dashboard that lists your weak spots, that's not acceptable. Use a dedicated app registration with assignment required and proper app roles (for example an Admin role), or at minimum an explicit viewer allow-list.

The frontend

Angular 21 with standalone components, signals, zoneless change detection and lazy routes. The main pages:

  • Dashboard: estate score and trend, critical/high failures, live outages, Front Door bypasses, top problems grouped by rule
  • App Services: each app's chain shown as cards. Click a stage to filter its checks, each with its fix
  • Resources: PaaS resources by type, with per-resource checks
  • Topology: Front Door profiles and WAF mode, VNets → subnets → apps and private endpoints, NSG tables with flagged rules highlighted
  • Admin: editable scoring rules (read-only for viewers) and scan settings

Signals and SSE pair well. The stream updates one signal, and the whole UI follows.

Hosting: where you probe from matters

Local Hosted
Azure access az login App Service managed identity with the Reader role
Data local data/ folder /home/data (persists across restarts)
Size — One Linux App Service, Always On, single instance

Two decisions worth calling out:

  • Reader is enough. Every check reads configuration only. No data plane, no secrets.
  • Keep it out of your VNets. The probes need to test reachability from the internet, the way an attacker would. Inside a VNet, private DNS gives answers that look reassuring but are misleading.

The same applies when running locally: if you're on a network whose IPs are allow-listed somewhere, those resources will look "reachable" from your machine. The probe's vantage point is part of the test.

Known limits

  • The SQL MI stage shows which instances an app can reach on the network, not which database it actually uses. Confirming that would mean reading Key Vault, which is deliberately out of scope.
  • Live state is held in memory, so it runs as a single instance.
  • Only Front Door Standard/Premium is analysed, not classic Front Door.

Takeaways

  1. Trace one request path by hand first. It tells you exactly what to automate.
  2. Probe from the outside. Configuration and reality drift apart.
  3. Cap your scores. Severity-weighted averages still hide critical gaps unless one failure can sink the score.
  4. Treat a security dashboard as sensitive. It's a map of your weak spots, so lock down who can sign in.
  5. Zero dependencies is achievable on the server side for a tool like this, and it's worth it.

If you've built something similar, or you rely on Defender for Cloud or Azure Policy for this, I'd like to hear how you handle the live-probe side. Let me know in the comments.

Top comments (0)