Skip to content
AccessLens

Dev Log

Dev Log: Week of February 13

Multi-tenant support, a policy scoring system, a What-If simulator bug that was silently giving wrong results, gap analysis upgrades, and a conflict detection overhaul.

Stephen Devlin · · 5 min read

This is the first dev log for AccessLens. The plan is to write these regularly, covering what I've been working on, what broke, what shipped, and what I learned along the way. No polish, just an honest account of building a security tool.

This was a heavy week. Multi-tenant support, a policy scoring system, a simulator bug that was silently giving wrong results, a gap analysis upgrade, and a conflict detection overhaul.

Multi-Tenant Support

This was the big one. And honestly, it should have been built this way from the start. AccessLens stores Conditional Access policies locally, but until now everything lived under a single namespace. If you connected two tenants, the second sync would overwrite the first. Nobody complained because most people only have one tenant. But if you're an MSP managing 15 tenants, that's a bit of a problem.

The solution was tenant-scoped storage. Every data key now includes the tenant ID, so each tenant's policies, settings, and sync state are completely isolated. A tenant switcher in the sidebar lets you flip between them, and all reads and writes go through tenant-aware paths.

The trickiest part was migrating existing data. I couldn't just flip a switch and break everyone's existing setup. Users who had already synced policies under the old unscoped format needed their data moved across without losing anything. Wrote a migration helper that detects legacy data, infers the tenant from the stored policies, and moves everything into the new scoped format. It's the kind of code you write once, pray works, and then never touch again.

Also added cross-device sync for tenant registrations, so switching browsers or machines doesn't mean re-connecting every tenant from scratch.

The What-If Simulator Bug

This one kept me up. The What-If simulator evaluates what would happen if a user tried to sign in under specific conditions. When a Conditional Access policy uses an OR grant operator (e.g. "Require MFA OR compliant device OR domain-joined device"), the simulator needs to evaluate whether any of those controls would be satisfied.

The bug: if the simulator couldn't determine whether a control was met (e.g. it doesn't know if the user has completed MFA because that happens at sign-in), it was treating the unknown as "Not met". For an OR policy, that meant all unknowns resulted in "Access Blocked". That's just wrong. If any one control could potentially be satisfied, the result should be "Granted with Requirements". The worst part? This had been live for weeks. Nobody reported it because the results looked plausible enough. That's the danger of subtle logic bugs in security tooling.

// Before: unknowns treated as failures
// OR with [unknown, unknown, met] -> blocked (wrong)

// After: unknowns treated as requirements
// OR with [unknown, unknown, met] -> granted with controls

The fix was straightforward, but the bug had been silently producing incorrect results for any policy using OR grant operators with controls the simulator couldn't evaluate. Updated the UI to show unknown controls as "Required" with an amber warning icon instead of a red block.

Gap Analysis: Report-Only Awareness

The gap analysis engine checks your tenant against a list of security best practices: do you have MFA enforced for admins, are legacy auth protocols blocked, is device compliance required for sensitive apps, and so on. Each check returns a pass, fail, or partial result.

The problem was that it treated report-only policies as if they didn't exist. If you had a policy that blocks legacy authentication but it was still in report-only mode (which is the recommended approach before enforcing), the gap analysis would flag it as a complete miss. That's misleading. You've done the work. You're just validating before flipping the switch.

The fix was adding a report-only fallback layer. If a check fails because no enforced policy covers it, the engine now looks for report-only policies that would satisfy the check if they were enforced. When it finds one, it reports "Partial coverage" with a note explaining the policy exists in report-only and is ready to enforce. Much more useful than a flat "Not covered" for an admin who's halfway through a rollout.

Also added documentation links to each gap check. Every result now includes a direct link to the relevant Microsoft Learn article, so you're not left guessing what the recommendation actually means or how to implement it. Small thing, but it cuts out a surprising amount of back-and-forth for people who are new to Conditional Access.

Policy Scoring and the Matrix Overhaul

This one had been brewing for a while. The Policy Matrix page gives you a bird's-eye view of your Conditional Access coverage, but until now it was binary. Either a dimension was covered or it wasn't. That's useful for spotting obvious gaps, but it doesn't tell you anything about the quality of coverage.

Built a per-dimension scoring system that evaluates policy strength across multiple axes: user scope breadth, application coverage, grant control strength, session controls, platform conditions, location requirements, and whether the policy is actually enforced or still in report-only. Each dimension gets a weighted score, and the overall policy strength is a composite of all of them.

The interesting challenge was calibrating the weights. A policy that requires MFA for all users across all apps is strong. But a policy that requires MFA for a single user on a single app, while technically "covering" MFA, isn't providing much protection. The scoring needed to reflect that difference without being so aggressive that narrow, targeted policies look useless. They have their place.

Wired the scores into the coverage radar chart and the policy heatmap, so you can now see at a glance not just where your gaps are, but where your coverage is thin. The heatmap went from a green/red binary to a gradient that actually means something.

Conflict Detection Overhaul

The conflict detection engine got a significant upgrade. Previously it would tell you "Policy A and Policy B conflict" with a vaguely helpful description. Useful, but about as actionable as a smoke alarm that just says "something's on fire somewhere". Now it produces structured overlap breakdowns showing exactly which axes overlap (users, apps, conditions) and generates specific recommendations for each conflict.

Added a new detection type: Control Escalation. This catches cases where two policies target the same users and apps but one requires stronger controls than the other. For example, one policy requires MFA while another requires MFA plus a compliant device. The weaker policy is effectively redundant for users in the overlap. That's not necessarily wrong, but you should know about it.

Also added CSV export for the conflicts page, a "grouped by policy" view, and the ability to mark conflicts as "Accepted Risk" so they don't clutter the view on subsequent reviews. That last one was a user request. Turns out people don't love seeing the same 12 known conflicts every time they open the page.

What's Next

Next week I'm working on the blog system (you're reading the result of that) and starting on the remaining sidebar layout redesigns. Also need to revisit the Baseline Builder's cross-framework progress sync, which has some edge cases around policy completion states that aren't propagating correctly. That one's been on the list for a while. Keeps getting bumped by things that are actually broken.

Share this post
LinkedIn