By the end of this you have Microsoft’s security baseline assigned in audit across a Northfork subscription, a compliance result read by control rather than by resource, and a shortlist of the controls that are actually yours to fix separated from the ones Microsoft answers for.
How this was verified: every command, setting and value below is taken from current Microsoft documentation, checked against the product’s own command reference in August 2026, and shaped by estates I have built. The sequence has not been executed end to end as a single unbroken lab run for this article. Where a step is more likely than others to differ at a console, it says so in place.
Before you start
You need Resource Policy Contributor or Owner on the subscription, the Azure CLI, and an estate with some real resources in it, because a baseline assigned against an empty subscription teaches you nothing. Nothing here enforces anything or changes a resource. Every effect in this build is a reporting effect and the assignment can be deleted at any point without consequence.
If your platform came from Azure Landing Zones, this initiative is very likely already assigned at your intermediate root. Assigning it again at a subscription is still the right exercise, because the point of this build is learning to read the result, and a second assignment in audit costs nothing and conflicts with nothing.
SUB_ID=$(az account show --query id -o tsv)
SCOPE="/subscriptions/$SUB_ID"
ASSIGN="NFK-Asgn-Baseline"
The naming convention, set once for the whole series
Every object created across this series follows one pattern, NFK-Type-Domain-Control, where the type token is one of Def, Init, Asgn, Exm or Att. Substitute your own organisation prefix for NFK. The type token in the middle earns its place for a practical reason: definitions, initiatives, assignments, exemptions and attestations all appear in the same search box and the same JSON exports, and without a token you cannot tell from a name which kind of object you are holding.
| Object | Example | Why |
|---|---|---|
| Policy definition | NFK-Def-Storage-BlockAnonymousBlob | Names the control, never the effect, because the effect is chosen at assignment time and the name would go stale. |
| Initiative | NFK-Init-Standard | Named for the domain or the standard, so later rules join it without a rename. |
| Assignment | NFK-Asgn-Baseline | What is assigned plus where, since the same initiative lands in several places with different parameters. |
| Exemption | NFK-Exm-Storage-Corp-Legacy | Assignment plus reason, so an expired exemption can be judged from its name alone. |
| Attestation | NFK-Att-<control reference> | Ties the claim to the control it answers for. |
Assignment names are limited to 24 characters at management group scope, and that is the constraint that breaks most naming schemes designed on a whiteboard. Check the length of your longest name before committing to a pattern rather than after.
Step 1. Find the initiative, and read its size before anything else
Resolve the initiative from the tenant rather than from a GUID written down somewhere. Microsoft has renamed this baseline once already, and a build sheet that prints an identifier is a build sheet that ages badly.
az policy set-definition list \
--query "[?policyType=='BuiltIn' && contains(displayName, 'security benchmark')].{name:name, display:displayName}" \
--output table
Take the name from that output and use it for the rest of the build. Then read how large the thing is before you assign it, which is the step that changes how people talk about the result afterwards.
BASELINE=<the name from the previous command>
az policy set-definition show --name "$BASELINE" \
--query "{display:displayName, version:version, count:length(policyDefinitions)}" \
--output json
Expected output is a display name, a version, and a count in the low hundreds. That number is the point of the step. You are not assigning a rule, you are assigning a programme of work, and the compliance percentage you get tomorrow is a fraction whose denominator you have now seen.
Step 2. Find out how much of it is not yours
Before assigning, count the member definitions by policy type. Anything marked Static is a Microsoft-owned control whose result comes from third-party audits of Microsoft’s own infrastructure, and no action you take will move it.
az policy set-definition show --name "$BASELINE" \
--query "policyDefinitions[].policyDefinitionId" -o tsv \
| while read -r ID; do
az policy definition show --name "${ID##*/}" --query policyType -o tsv 2>/dev/null
done | sort | uniq -c
This takes a few minutes and it is worth running once in your life. The output is a count per policy type. Record it. When somebody later asks why the compliance figure will not reach a hundred percent, that number is a large part of the answer and it is much better delivered in advance than defensively.
Step 3. Assign it, in audit
az policy assignment create \
--name "$ASSIGN" \
--display-name "Northfork security baseline (assessment only)" \
--scope "$SCOPE" \
--policy-set-definition "$BASELINE" \
--description "Assessment assignment. No enforcing effects. Owner: platform team."
| Setting | Value | Why |
|---|---|---|
| Scope | One subscription | Start where the blast radius is understood. The baseline belongs at a management group eventually, and that is a decision to make after you have seen what it reports. |
| Parameters | None supplied | Deliberate. The defaults are what most estates run, and the first pass should measure the estate against the shipped defaults rather than against a tuned version you have not justified yet. |
| Enforcement mode | Default | Not DoNotEnforce. This initiative’s members are audit and manual effects, so there is nothing to suppress, and setting it would only obscure what the assignment is doing. |
| Description | Names the purpose and the owner | Somebody will find this assignment in six months and need to know whether it can be deleted. |
Step 4. Wait properly, then force the scan
An assignment takes roughly five minutes to apply and a newly assigned scope produces its first compliance results in around thirty. A scan triggered before the assignment has applied returns a confident and empty answer, which is the most common way people conclude that a baseline found nothing wrong.
sleep 300
az policy state trigger-scan --no-wait
# then allow the scan to complete before reading; on a populated
# subscription this initiative takes a while
Step 5. Read it by control, not by resource
The default instinct is to list non-compliant resources, which on a real subscription returns thousands of rows and tells you nothing actionable. Group by the definition instead. One control failing across four hundred resources is a single decision; four hundred resources each failing one control is a fantasy that does not occur.
az policy state summarize --scope "$SCOPE" \
--filter "policyAssignmentName eq '$ASSIGN'" \
--query "value[0].policyAssignments[0].policyDefinitions[?results.nonCompliantResources > \`0\`].{
definition: policyDefinitionReferenceId,
failing: results.nonCompliantResources
}" \
--output table
Expected output is a list of definition reference identifiers with a count against each, sorted however the service returns them. This is your actual worklist and it is usually far shorter than people expect, because failures cluster hard: a handful of controls generate most of the non-compliance in almost every estate I have looked at.
A reference identifier is not a readable name. Resolve the ones you care about rather than guessing from the string.
REF=<a policyDefinitionReferenceId from the previous output>
az policy set-definition show --name "$BASELINE" \
--query "policyDefinitions[?policyDefinitionReferenceId=='$REF'].policyDefinitionId" -o tsv \
| xargs -I{} az policy definition show --name "$(basename {})" \
--query "{display:displayName, type:policyType, effects:parameters.effect.allowedValues}" -o json
Three things to read off that. The display name tells you what the control is. The policy type tells you whether it is yours: Static means stop, this one is Microsoft’s. And the allowed effect values tell you whether this control could ever be enforced, or whether it is assessment-only by construction, which decides whether “fix it” is even a thing you can automate later.
Step 6. Triage into three piles
Work down the list from step 5 and sort every failing control into one of three outcomes. This is the deliverable of the whole build, and it is a document rather than a command.
| Pile | Test | What happens next |
|---|---|---|
| Fix | The control is right, the estate is wrong, and remediation is possible | Goes to a remediation task or a workload team. The remediation cluster later in this series is the mechanism. |
| Accept | The control is right and you are choosing not to meet it yet | Becomes an exemption with a category, an owner and an expiry date. Never leave it as a red number nobody has decided about. |
| Differ | The control is nearly right but the threshold, scope or mode does not fit | This is the customisation pile, and it is the specification for the next act of this series. Write down what specifically does not fit, in the moment, because that sentence is worth more later than any workshop. |
Anything Static goes in none of these piles. It is Microsoft’s control and your only action is to know it is there.
Step 7. The end-to-end test
Pick one failing control from the Fix pile, find one resource it flagged, and confirm by hand that the resource really is in the state the policy claims. Reading a compliance result is trusting a report; this checks it against the thing itself.
az policy state list --scope "$SCOPE" \
--filter "policyAssignmentName eq '$ASSIGN' and policyDefinitionReferenceId eq '$REF' and complianceState eq 'NonCompliant'" \
--query "[0].resourceId" -o tsv
Take the resource identifier, open the resource, and verify the specific property the definition inspects. Expected result is that the property is genuinely in the state reported. If it is not, you have found either a rule matching something other than what its name suggests, or a stale evaluation, and both are worth knowing before you act on four hundred rows of the same finding.
Step 8. Keep it or remove it
An audit-only baseline assignment is worth keeping permanently, and I would leave it in place. If this was a one-off exercise, removing it takes one command and leaves nothing behind, because nothing here created an identity or changed a resource.
az policy assignment delete --name "$ASSIGN" --scope "$SCOPE"
From here
You have measured the estate against rules somebody else wrote and produced three piles. The next articles work through what each pile needs: the effects and guardrails for the things you will enforce, remediation for the things you will fix, and then the two harder questions, what to do when the built-in is nearly right, and what to do when nothing exists at all.
Azure Policy
‹ Previous: [AP 3] What Microsoft Already Ships: Reading the Catalogue
Next: [AP 4] Effects: What Each One Commits You To ›




