martinpronk.com
Initiative Merger: Cleaning Up the Azure Policy Compliance Mess

Initiative Merger: Cleaning Up the Azure Policy Compliance Mess

Azure Policy has no built-in deduplication. Assign five compliance frameworks and you get five times the noise. Initiative Merger fixes that by merging multiple initiatives into one deduplicated, deployable initiative.

· 7 min read

If you have worked with multiple Azure compliance frameworks simultaneously — MCSB, CIS, ISO 27001, NIST, BIO — you have run into the same wall. Assign all of them to a subscription and the compliance dashboard fills up with hundreds of overlapping findings. The same policyDefinitionId fires from CIS and from MCSB. The same storage account appears non-compliant five times in a row, once per framework.

This is not a configuration problem. It is how Azure Policy works: every assigned initiative is evaluated independently, with no awareness that another initiative already covers the same policy. The compliance score counts every hit, even duplicates.

Initiative Merger is an open-source .NET tool I built to fix this. It fetches the initiative definitions, deduplicates them on policyDefinitionId, resolves parameter conflicts between frameworks, and generates a single ready-to-deploy initiative JSON. The result is one initiative with only unique policies, one compliance score that actually means something, and a findings list where every entry is distinct.

Why This Problem Exists

Azure Policy has two things that do not talk to each other: the initiative (a policySetDefinition) and the evaluation engine. When you assign three initiatives and all three reference the same policyDefinitionId, the engine evaluates that policy three times per resource. Three non-compliant results. Three entries in your findings list. Three deductions from three separate scores.

Stacking five real-world frameworks makes the numbers ugly quickly:

FrameworkPolicies
MCSB~223
CIS~108
ISO 27001~450
NIST SP 800-53 Rev. 5~696
NL BIO Cloud Theme V2~282

Naïve total: around 1,759 policies. After deduplication the overlap becomes immediately visible, because NIST alone covers most of what MCSB and CIS already include. Your compliance dashboard is not showing you how well you are doing; it is showing you how many times each problem was counted.

What Initiative Merger Does

The core operation is straightforward: fetch the initiative definitions via the Azure CLI, deduplicate policy references using a HashSet<string> keyed on policyDefinitionId, preserve cross-framework control group mappings, resolve any parameter conflicts, then emit a single ARM-compatible JSON.

A few details are worth unpacking.

Deduplication with full group mapping

When a policy appears in both CIS and BIO, the first occurrence is kept but the group names from both frameworks are merged onto it. That means a policy can be tagged with CIS 7.4 and BIO 12.6.1 at the same time, so filtering the merged initiative by either control domain still works correctly. Dropping the duplicate occurrence without preserving its group names would silently lose the cross-framework mapping.

Parameter conflict resolution

Compliance frameworks often use the same parameter name with different default values. NIST may set effect to Audit while CIS sets it to Deny. The tool surfaces these conflicts and resolves them according to a configurable strategy:

StrategyBehaviour
PreferFirstUse the value from whichever framework was listed first
MostRestrictiveUse the lowest numeric value, false over true
UseDefaultOmit the parameter, letting Azure apply its own default
FailOnConflictAbort — any conflict requires manual action

AllowedValues conflicts are always resolved by union, regardless of strategy. Discarding valid values from the allowed set would make the parameter unusable after assignment.

Parameter pruning

After deduplication, a policy that was dropped as a duplicate can leave its parameters orphaned at the initiative level. Azure rejects an initiative with parameters not referenced by any policy definition (UnusedPolicyParameters error). The tool walks the remaining definitions, collects all referenced parameter names via a regex on [parameters('name')] expressions, then removes any initiative-level parameter that is no longer referenced.

This same pruning runs again after the group filter step, so filtering down to a subset of controls also cleans up the parameter list.

Azure limits

Merged initiatives can bump into Azure’s hard limits: 1,000 policy definitions and 400 parameters per initiative. The tool validates the merge result and emits warnings (or errors if limits are exceeded) before deployment:

  • Over 800 policies: warning to filter controls
  • Over 1,000 policies: error, deployment blocked
  • Over 350 parameters: warning to apply aliases
  • Over 400 parameters: error, deployment blocked

If the parameter count is high, the tool detects alias candidates: initiative-level parameters that appear to represent the same value under different names (for example listOfAllowedLocations and allowedLocations). You can approve these aliases to collapse them into a single parameter, reducing the count without changing the effective policy behaviour.

Architecture

The solution is split into three projects:

src/
├── InitiativeMerger.Core/     # Domain logic, no external dependencies
├── InitiativeMerger.Web/      # Blazor web UI + REST API
└── InitiativeMerger.Cli/      # Standalone CLI tool

The core library has no dependency on web frameworks or CLI libraries, only on System.Text.Json and the Microsoft.Extensions logging abstraction. The web and CLI projects are thin wrappers around the same services.

Azure CLI (az) is used for all Azure interactions — fetching initiative definitions and deploying the result. The tool never holds credentials itself; it relies on whatever az authentication is active in the current shell. All CLI calls use ProcessStartInfo.ArgumentList (array-based argument passing), so initiative IDs are never interpolated into a command string.

Using It

Requirements

  • .NET 10 SDK
  • Azure CLI (az in PATH)
  • Read access to policy definitions
  • Policy Contributor or Owner role if deploying

CLI

# List known frameworks
dotnet run --project src/InitiativeMerger.Cli -- --list-known

# Merge MCSB and CIS, write to a file
dotnet run --project src/InitiativeMerger.Cli -- \
  --keys MCSB CIS \
  --name "Hardened Baseline" \
  --output merged.json

# Merge all five, deploy directly to a subscription
dotnet run --project src/InitiativeMerger.Cli -- \
  --keys MCSB CIS ISO27001 NIST BIO \
  --name "Full Compliance Framework" \
  --conflict-strategy MostRestrictive \
  --deploy \
  --subscription xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

Custom initiative IDs (any policySetDefinition resource ID or display name) can be added via --ids. This makes it easy to include in-house initiatives alongside the well-known frameworks.

Web UI

dotnet run --project src/InitiativeMerger.Web

The Blazor interface walks you through the same options in a step-by-step flow: pick frameworks, add custom IDs, configure the name and conflict strategy, start the merge. The result page shows the deduplication statistics, any parameter conflicts and how they were resolved, and gives you a group filter where you can include or exclude specific control domains before downloading or deploying.

The group filter is useful when the full merge exceeds Azure’s policy count limit. Selecting only the ISO 27001 access management controls together with the MCSB baseline gives you a smaller, more focused initiative than stacking everything.

REST API

The web application also exposes a REST endpoint, useful if you want to call this from a pipeline without running the CLI:

POST /api/initiative/merge
Content-Type: application/json

{
  "wellKnownKeys": ["MCSB", "CIS"],
  "customInitiativeIds": [],
  "outputDisplayName": "Hardened Baseline",
  "conflictResolution": "MostRestrictive",
  "deployToAzure": false
}

Bicep Output

Beyond the ARM JSON, the tool can generate a Bicep file for the merged initiative, which fits better in a modern IaC workflow. The generated Bicep:

  • Targets subscription scope
  • Declares the policySetDefinitions resource with all properties inlined
  • Exposes only the parameters that have no default value as Bicep parameters
  • Optionally includes a second resource block for the policy assignment, with a SystemAssigned identity if any DeployIfNotExists or Modify policies are included

This means you can drop the output directly into an existing Bicep repository without manually translating the ARM JSON.

What It Does Not Do

A few things worth being explicit about.

The tool does not replace a policy-as-code workflow. If you are already using EPAC or a Terraform-based setup, you should generate the initiative JSON once and commit it to your repository like any other resource definition. Initiative Merger produces the input to that workflow, not a replacement for it.

It does not make compliance frameworks meaningful. If the underlying frameworks overlap in ways that produce noise rather than signal, a deduplicated initiative is cleaner noise. The tool solves the duplicate-counting problem, not the problem of choosing the right policies in the first place.

It does not handle exemptions. Existing exemptions tied to specific initiatives will need to be reviewed if you replace five initiatives with one. The scope and policy definition ID references in exemptions are not migrated automatically.

Getting Started

The repository is at github.com/martinpronk/InitiativeMerger.

git clone https://github.com/martinpronk/InitiativeMerger.git
cd InitiativeMerger
dotnet build
dotnet run --project src/InitiativeMerger.Cli -- --list-known

That last command will list the five built-in frameworks and their resource IDs, confirming that the Azure CLI connection is working.

If the multi-framework overlap is already a problem in your environment, the quickest starting point is a merge of MCSB and CIS with --conflict-strategy MostRestrictive and --output merged.json. Review the conflict report, check the statistics, and compare the resulting policy count against what you have assigned today.