Automate SOC 2 Compliance with PowerShell

Published:29 June 2026 - 7 min. read

Audit Active Directory for stale users, weak passwords, and other security risks with Specops Password Auditor.

SOC 2 audits can feel like a recurring scavenger hunt. Someone asks for proof that cloud resources are tagged, logging is enabled, storage is restricted, and security findings are being reviewed. Then the IT team starts taking portal screenshots and exporting one-off CSV files.

That approach works until your Azure footprint grows, your auditor asks for a larger sample, or a control owner changes jobs. A better approach is to treat compliance evidence like any other operational artifact: define the control, enforce it where possible, collect evidence on a schedule, and keep the output in a repeatable format.

In this tutorial, you will use PowerShell, Azure Policy as Code, and Azure compliance data to build a practical SOC 2 compliance automation workflow for Azure. You will not “become SOC 2 compliant” by running a script. SOC 2 is based on the AICPA Trust Services Criteria and requires auditor judgment, process evidence, and organizational controls. But you can automate a large portion of the cloud evidence collection and governance work that supports those controls.

Azure Policy compliance dashboard showing compliant and non-compliant resources

Prerequisites

To follow along, you will need the following:

  • An Azure subscription where you can read resources and assign Azure Policy.

  • PowerShell 7 or Windows PowerShell with the Az PowerShell module installed.

  • Permission to create policy definitions and assignments at the subscription or management group scope.

  • A folder where you can store Policy as Code files and audit exports.

Install the Az module if you do not already have it.

Install-Module -Name Az -Repository PSGallery -Scope CurrentUser -Force
Import-Module Az
Connect-AzAccount

After signing in, set the subscription you want to work with.

$SubscriptionId = '00000000-0000-0000-0000-000000000000'
Set-AzContext -SubscriptionId $SubscriptionId

Replace the placeholder subscription ID with your own Azure subscription ID before running any examples.

Mapping SOC 2 Controls to Azure Evidence

Before writing a script, decide what evidence you are collecting. SOC 2 reports are organized around Trust Services Criteria such as security, availability, confidentiality, processing integrity, and privacy. Azure will not know your control wording, but Azure can provide evidence for many cloud-focused control activities.

For example, a practical evidence map might look like this:

SOC 2 control area Azure evidence to collect
Change management Azure Policy definitions and assignments stored in source control
Asset inventory Azure resources tagged with owner, environment, and data classification
Security monitoring Microsoft Defender for Cloud secure score and recommendations
Configuration governance Azure Policy compliance state by resource
Audit trail Activity logs and exported compliance CSV files

Start small. Pick three to five controls that produce clear Azure evidence. For this walkthrough, you will enforce tagging, collect Azure Policy compliance state, and export Defender for Cloud security posture data.

Creating a Policy as Code Folder

Azure Policy as Code means your policy definitions, assignments, and parameters live in files instead of being hand-created in the portal. Microsoft’s Azure Policy documentation describes policy definitions, initiatives, and assignments as the core objects used to implement governance.

Create a simple repository structure.

New-Item -ItemType Directory -Path .\soc2-policy\definitions -Force
New-Item -ItemType Directory -Path .\soc2-policy\assignments -Force
New-Item -ItemType Directory -Path .\soc2-evidence -Force

You can keep this folder in Git so every policy change has a pull request, reviewer, and history. That history becomes change-management evidence by itself.

Writing a Required Tag Policy

Tags are not glamorous, but they make evidence collection far easier. If every production resource has an owner, environment, and data classification tag, you can scope reports by system and control owner.

Create a file named .\soc2-policy\definitions\require-soc2-tags.json with the following policy definition.

{
  "properties": {
    "displayName": "SOC 2 - Require owner, environment, and data classification tags",
    "policyType": "Custom",
    "mode": "Indexed",
    "description": "Audits resources missing required SOC 2 evidence tags.",
    "metadata": {
      "category": "SOC 2"
    },
    "parameters": {
      "effect": {
        "type": "String",
        "metadata": {
          "displayName": "Effect"
        },
        "allowedValues": [
          "Audit",
          "Deny",
          "Disabled"
        ],
        "defaultValue": "Audit"
      }
    },
    "policyRule": {
      "if": {
        "anyOf": [
          { "field": "tags['owner']", "exists": "false" },
          { "field": "tags['environment']", "exists": "false" },
          { "field": "tags['dataClassification']", "exists": "false" }
        ]
      },
      "then": {
        "effect": "[parameters('effect')]"
      }
    }
  }
}

This policy uses Audit as the default effect. Audit mode is a good starting point because you can measure drift before blocking deployments. Once teams understand the requirement, you can assign the same policy with Deny for production scopes.

Publish the policy definition to Azure.

$PolicyDefinition = New-AzPolicyDefinition `
  -Name 'soc2-require-evidence-tags' `
  -DisplayName 'SOC 2 - Require evidence tags' `
  -Policy '.\soc2-policy\definitions\require-soc2-tags.json' `
  -Mode Indexed

Assign the policy to the current subscription.

$Scope = "/subscriptions/$SubscriptionId"

New-AzPolicyAssignment `
  -Name 'soc2-require-evidence-tags' `
  -DisplayName 'SOC 2 - Require evidence tags' `
  -Scope $Scope `
  -PolicyDefinition $PolicyDefinition

Azure Policy assignment for required SOC 2 tags

Collecting Azure Policy Compliance Evidence

Now comes the audit-friendly part: collecting evidence with PowerShell. The Get-AzPolicyState cmdlet returns policy compliance states for resources. You can query the latest state, filter non-compliant resources, select fields, and export the results.

Create a script named Export-Soc2PolicyEvidence.ps1.

param(
    [Parameter(Mandatory)]
    [string]$SubscriptionId,

    [string]$OutputFolder = '.\soc2-evidence'
)

$ErrorActionPreference = 'Stop'

Set-AzContext -SubscriptionId $SubscriptionId | Out-Null
New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null

$Timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$PolicyCsv = Join-Path $OutputFolder "policy-compliance-$Timestamp.csv"

Get-AzPolicyState `
    -SubscriptionId $SubscriptionId `
    -Filter "ComplianceState eq 'NonCompliant'" `
    -Select 'Timestamp,ResourceId,PolicyAssignmentName,PolicyDefinitionName,ComplianceState' |
    Export-Csv -Path $PolicyCsv -NoTypeInformation

Write-Output "Policy evidence exported to $PolicyCsv"

Run the script.

.\Export-Soc2PolicyEvidence.ps1 -SubscriptionId $SubscriptionId

Open the CSV and you have a repeatable list of non-compliant resources at a point in time. That output is far more useful than a screenshot because you can diff it across weeks, route it to resource owners, and prove remediation trends.

CSV export showing non-compliant Azure Policy resources

Adding Defender for Cloud Secure Score Evidence

Azure Policy tells you whether resources comply with assigned governance rules. Microsoft Defender for Cloud adds security posture context, including secure score information and regulatory compliance views.

Use the Az.Security cmdlet Get-AzSecuritySecureScore to export secure score data for the subscription. JSON is a good format here because secure score objects can include nested result data.

$Timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$SecureScoreJson = ".\soc2-evidence\secure-score-$Timestamp.json"

Get-AzSecuritySecureScore |
    ConvertTo-Json -Depth 10 |
    Out-File -FilePath $SecureScoreJson -Encoding utf8

Secure score is not a SOC 2 control by itself. Treat it as supporting evidence for your Azure security posture, especially around vulnerability management, configuration monitoring, and continuous improvement.

Microsoft Defender for Cloud secure score summary

Building a Single Evidence Export Script

Once the individual pieces work, combine them into one scheduled script. The following example exports policy non-compliance and secure score evidence into a date-stamped folder.

param(
    [Parameter(Mandatory)]
    [string]$SubscriptionId,

    [string]$EvidenceRoot = '.\soc2-evidence'
)

$ErrorActionPreference = 'Stop'

Set-AzContext -SubscriptionId $SubscriptionId | Out-Null

$RunStamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$RunFolder = Join-Path $EvidenceRoot $RunStamp
New-Item -ItemType Directory -Path $RunFolder -Force | Out-Null

$PolicyPath = Join-Path $RunFolder 'azure-policy-noncompliance.csv'
$SecureScorePath = Join-Path $RunFolder 'defender-secure-score.json'

Get-AzPolicyState `
    -SubscriptionId $SubscriptionId `
    -Filter "ComplianceState eq 'NonCompliant'" `
    -Select 'Timestamp,ResourceId,PolicyAssignmentName,PolicyDefinitionName,ComplianceState' |
    Export-Csv -Path $PolicyPath -NoTypeInformation

Get-AzSecuritySecureScore |
    ConvertTo-Json -Depth 10 |
    Out-File -FilePath $SecureScorePath -Encoding utf8

[pscustomobject]@{
    SubscriptionId = $SubscriptionId
    EvidenceRun    = $RunStamp
    PolicyEvidence = $PolicyPath
    SecureScore    = $SecureScorePath
} | ConvertTo-Json | Out-File -FilePath (Join-Path $RunFolder 'manifest.json') -Encoding utf8

Write-Output "SOC 2 evidence exported to $RunFolder"

A manifest file is a small but helpful addition. It gives auditors and internal reviewers a predictable starting point for each evidence run.

Scheduling Evidence Collection

You can run the script manually during audit prep, but the real value comes from scheduling it. In production, run the export from a secure automation host, Azure Automation account, GitHub Actions workflow, or CI/CD runner using a managed identity or service principal with the least permissions required.

A practical schedule is weekly for normal monitoring and daily during audit remediation windows. Store exports in a protected location such as a private storage account, restricted SharePoint library, or evidence repository. Keep retention aligned with your audit period and company policy.

Do not give the automation identity broad owner rights just because it is convenient. For evidence collection, read permissions and policy insight access are normally enough. For policy deployment, use a separate deployment identity with tightly controlled approvals.

Making the Output Auditor-Friendly

Automation can produce too much data. Your job is to make the evidence easy to review.

For each evidence run, include:

  • The script version or Git commit that produced the export.

  • The Azure subscription or management group scope.

  • The date and time of collection.

  • CSV exports for the raw data.

  • A short summary of open exceptions and assigned owners.

If your required tag policy finds 200 non-compliant resources, do not bury the result in a folder and call it done. Create a remediation workflow. Assign each resource to the tagged owner where possible, track the exception, and re-run the report after remediation.

Common Pitfalls

Avoid these mistakes when automating SOC 2 compliance evidence:

  • Treating automation as the control owner. A script collects evidence. People still own control design, review, and remediation.

  • Skipping source control. If policies are edited only in the portal, you lose review history and change evidence.

  • Exporting secrets. Review every export before storing it in an audit evidence repository.

  • Using one giant policy initiative on day one. Start with high-confidence controls, then expand.

  • Ignoring exceptions. An exception without owner, reason, approval, and expiration date is audit debt.

Conclusion

SOC 2 compliance automation is not about replacing auditors or turning a framework into a checkbox script. It is about making your Azure environment continuously measurable. PowerShell gives you a repeatable way to collect evidence, Azure Policy as Code gives you versioned governance, and Defender for Cloud helps you monitor Azure security posture.

Start with one control, such as required evidence tags. Store the policy in Git, assign it in audit mode, export non-compliance with Get-AzPolicyState, and review the CSV on a schedule. Once that workflow is trusted, add more policies, secure score exports, and remediation reporting.

Manual audit checklists will always have a place, but they should not be your first source of truth. Let Azure and PowerShell do the repetitive work so your team can focus on fixing the findings.

Sources

  • https://learn.microsoft.com/en-us/azure/governance/policy/overview

  • https://learn.microsoft.com/en-us/azure/governance/policy/concepts/policy-as-code

  • https://learn.microsoft.com/en-us/powershell/module/az.policyinsights/get-azpolicystate

  • https://learn.microsoft.com/en-us/powershell/module/az.security/get-azsecuritysecurescore

  • https://learn.microsoft.com/en-us/azure/defender-for-cloud/regulatory-compliance-dashboard

  • https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/tag-resources

  • https://learn.microsoft.com/en-us/powershell/azure/install-azure-powershell

  • https://www.aicpa-cima.com/resources/landing/system-and-organization-controls-soc-suite-of-services

Hate ads? Want to support the writer? Get many of our tutorials packaged as an ATA Guidebook.

Explore ATA Guidebooks

Looks like you're offline!