June 24, 2026
Chapter 3 — SOC 2 Type 2: From Risk Register to Audit-Ready Evidence
Part of the “Attack → Detect → Govern” series. Read the intro here, Chapter 1 here, and Chapter 2 here.
By DarkLightSec
12 min read
"SOC 2 isn't about checking boxes. It's about proving your controls actually worked consistently over time."
In Chapter 2, we built a risk register that connects technical findings to business decisions. Now let's close the loop: how do those risks translate into auditable controls? How do you prove and not just claim that your security posture held up for six to twelve months straight?
SOC 2 Type 2 is the answer the market demands. Unlike Type 1 (a point-in-time snapshot), Type 2 requires evidence that your controls didn't just exist on the day of the audit. They functioned as designed, every single day, across the entire audit window.
This chapter is a technical implementation guide. We'll cover the Trust Services Criteria (TSC) controls you actually need to satisfy, how to collect the right evidence, and how to connect your risk register entries to specific controls, so when the auditor arrives, you hand them a package, not a prayer.
3.1 Understanding the Trust Services Criteria
The American Institute of CPAs (AICPA) defines the Trust Services Criteria as the framework auditors use to evaluate your controls. There are five categories: Security (CC series), Availability (A series), Confidentiality (C series), Processing Integrity (PI series), and Privacy (P series).
Security is mandatory for every SOC 2 examination. The others are optional; you include them only if they're in scope for your service commitments. Most companies start with Security only. If you're handling sensitive health or financial data, Availability and Confidentiality often get added.
Here's what the Security (CC) controls actually require in practice, and what evidence satisfies each one.
CC1 — Control Environment (The Foundation, sometimes overlooked)
CC1 is about culture and governance which is the tone set from the top. Auditors want to see that your organisation treats security as a real commitment, not just a decorative wall hanging.
CC1.1 — Commitment to Integrity and Ethical Values
This sounds like a subjective cultural requirement, requires rigorous documented evidence. You need: a documented Code of Conduct (version-controlled, with acknowledgement logs showing every employee signed it); security awareness training records with completion timestamps; and background check documentation for employees in sensitive roles.
The common gap here is that companies complete training but have no system of record. An email to employees doesn't prove completion. You need an LMS or HR platform that logs who completed what and when.
CC1.2 — Board Oversight
Auditors want to see that leadership is actively reviewing security posture and not just blindly signing off on it. Evidence includes audit committee meeting minutes referencing security topics, risk register reviews with timestamps, and board-level access to security reports. Your GRC platform (Eramba, OneTrust, Drata) or an equivalent centralized compliance tracker should log these review activities automatically.
CC3 — Risk Assessment
CC3.1 and CC3.2 require you to demonstrate a formal, repeatable process for identifying and scoring risks. If you've followed Chapter 2, you're most of the way there. What auditors want to see is:
- A risk register with dated entries and review history showing quarterly updates
- Evidence that risk scores are calculated consistently, not arbitrarily
- Asset inventory that's up to date, auditors will cross-reference your risks against your actual environment
The key document is the risk register itself. Every entry should have a timestamp on when it was created, when it was last reviewed, and who owns it. Version control your register in Git or your GRC platform, that diff history is itself evidence of an active program.
CC6 — Logical and Physical Access Controls
This is where most auditors spend the majority of their time, and where most audit findings originate.
CC6.1 — Logical Access Controls
The requirement: only authorised users can access your systems, and that access is enforced by technical controls and not just policy.
What you need to produce (An example using Entra ID):
Conditional Access policy exports. In Azure AD / Entra ID, go to Security → Conditional Access → Policies. Export each policy to JSON. The auditor wants to see that MFA is enforced, that sign-in risk levels trigger step-up authentication, and that break-glass accounts are excluded but monitored. A policy that says "enabled" but has a blanket exclusion group containing half your staff proves nothing.
MFA enrollment reports. Navigate to Entra ID → Users → Authentication methods → User registration details. Export this report. You want 100% MFA enrollment. Any user without MFA enrolled is a finding. If you're not at 100%, document why (contractor accounts, shared accounts) and show the compensating control.
Access review completion logs. Every quarter, someone needs to certify that every privileged role assignment is still justified. Entra ID Access Reviews (under Identity Governance) logs when reviews were initiated, who the reviewer was, and whether each assignment was approved or removed. That log is your evidence. Saying "we reviewed it" without a system record is not evidence.
Here's the evidence collection script that generates the artefacts you'll hand to the auditor:
Powershell
# Export Conditional Access Policies — run before each audit window
Connect-MgGraph -Scopes "Policy.Read.All"
$Policies = Get-MgIdentityConditionalAccessPolicy
$Policies | ConvertTo-Json -Depth 10 |
Out-File "evidence/CC6.1_CA_Policies_$(Get-Date -Format yyyyMMdd).json"
# Quick health check: which policies enforce MFA?
$Policies | Where-Object {
$_.GrantControls.BuiltInControls -contains "mfa"
} | Select-Object DisplayName, State | Format-Table# Export Conditional Access Policies — run before each audit window
Connect-MgGraph -Scopes "Policy.Read.All"
$Policies = Get-MgIdentityConditionalAccessPolicy
$Policies | ConvertTo-Json -Depth 10 |
Out-File "evidence/CC6.1_CA_Policies_$(Get-Date -Format yyyyMMdd).json"
# Quick health check: which policies enforce MFA?
$Policies | Where-Object {
$_.GrantControls.BuiltInControls -contains "mfa"
} | Select-Object DisplayName, State | Format-TableRun this script as part of a scheduled job and store the output in a timestamped evidence folder. The timestamp matters, it proves the policy existed and was in a given state on a specific date.
CC6.2 — Authentication Strength
Beyond "MFA is on", auditors evaluate the quality of your authentication controls. FIDO2 hardware keys and Microsoft Authenticator (push with number matching) are considered strong. SMS OTP is considered weak and increasingly scrutinised. Document your MFA method distribution and show a migration plan if SMS is still prevalent.
The KQL query below gives you a 30-day authentication failure summary — useful both for detecting active attacks (brute force, password spray) and for proving to auditors that you're actively monitoring authentication health:
KQL
// CC6.2 Evidence: Authentication failure analysis
// Run in Microsoft Sentinel or Log Analytics
SigninLogs
| where TimeGenerated > ago(30d)
| where ResultType != "0"
| summarize
FailureCount = count(),
DistinctUsers = dcount(UserPrincipalName),
FirstSeen = min(TimeGenerated),
LastSeen = max(TimeGenerated)
by IPAddress, FailureReason = ResultDescription
| where FailureCount > 10
| order by FailureCount desc// CC6.2 Evidence: Authentication failure analysis
// Run in Microsoft Sentinel or Log Analytics
SigninLogs
| where TimeGenerated > ago(30d)
| where ResultType != "0"
| summarize
FailureCount = count(),
DistinctUsers = dcount(UserPrincipalName),
FirstSeen = min(TimeGenerated),
LastSeen = max(TimeGenerated)
by IPAddress, FailureReason = ResultDescription
| where FailureCount > 10
| order by FailureCount descExport the results monthly and store them in your evidence folder. A consistent pattern of low-volume failures is normal. Sudden spikes, or failures from unfamiliar geographies, warrant investigation — and that investigation log is also evidence of CC7.2.
CC6.3 — Least Privilege
The principle is straightforward: users get the minimum access needed to do their job. The evidence requirement is harder. You must prove that privileged role assignments are justified, time-bound where possible, and reviewed regularly.
In Azure, the highest-risk roles are Owner, Contributor, and User Access Administrator at subscription scope. Anyone with these is a permanent blast radius if compromised.
The evidence you need:
Role assignment export with justification. Every privileged assignment should trace back to a ticket, a job requirement, or a formal access request. If you can't answer "why does this person have Owner on this subscription?", the auditor will flag it.
PIM (Privileged Identity Management) activation logs. PIM enforces Just-In-Time access: privileged roles are assigned but not active until someone explicitly activates them with a business justification. The activation log is your audit trail. It shows who requested which role, when, for how long, and with what justification.
Stale account identification. A role assignment to someone who hasn't logged in for 90 days is a risk indicator. The following script exports a prioritised remediation list:
Powershell
# Identify stale privileged role assignments
# Anything not activated in 90 days gets flagged for review
Connect-MgGraph -Scopes "RoleManagement.Read.All", "AuditLog.Read.All"
$PrivilegedRoles = @("Global Administrator", "Security Administrator",
"Exchange Administrator", "SharePoint Administrator")
foreach ($RoleName in $PrivilegedRoles) {
$Role = Get-MgDirectoryRole | Where-Object { $_.DisplayName -eq $RoleName }
if (-not $Role) { continue }
Get-MgDirectoryRoleMember -DirectoryRoleId $Role.Id | ForEach-Object {
$User = Get-MgUser -UserId $_.Id -Property DisplayName, UserPrincipalName, SignInActivity
[PSCustomObject]@{
Role = $RoleName
DisplayName = $User.DisplayName
UPN = $User.UserPrincipalName
LastSignIn = $User.SignInActivity.LastSignInDateTime
DaysSinceSignIn = if ($User.SignInActivity.LastSignInDateTime) {
(New-TimeSpan -Start $User.SignInActivity.LastSignInDateTime).Days
} else { "Never" }
}
}
} | Export-Csv "evidence/CC6.3_Stale_Privileged_Accounts_$(Get-Date -Format yyyyMMdd).csv" -NoTypeInformation# Identify stale privileged role assignments
# Anything not activated in 90 days gets flagged for review
Connect-MgGraph -Scopes "RoleManagement.Read.All", "AuditLog.Read.All"
$PrivilegedRoles = @("Global Administrator", "Security Administrator",
"Exchange Administrator", "SharePoint Administrator")
foreach ($RoleName in $PrivilegedRoles) {
$Role = Get-MgDirectoryRole | Where-Object { $_.DisplayName -eq $RoleName }
if (-not $Role) { continue }
Get-MgDirectoryRoleMember -DirectoryRoleId $Role.Id | ForEach-Object {
$User = Get-MgUser -UserId $_.Id -Property DisplayName, UserPrincipalName, SignInActivity
[PSCustomObject]@{
Role = $RoleName
DisplayName = $User.DisplayName
UPN = $User.UserPrincipalName
LastSignIn = $User.SignInActivity.LastSignInDateTime
DaysSinceSignIn = if ($User.SignInActivity.LastSignInDateTime) {
(New-TimeSpan -Start $User.SignInActivity.LastSignInDateTime).Days
} else { "Never" }
}
}
} | Export-Csv "evidence/CC6.3_Stale_Privileged_Accounts_$(Get-Date -Format yyyyMMdd).csv" -NoTypeInformationAny account showing "Never" or 90+ days should be reviewed immediately. Removing them is an easy win before the audit window opens.
CC7 — System Operations and Monitoring
CC7.1 — Security Event Monitoring
Auditors look for active monitoring. A static dashboard screenshot isn't sufficient, proof is required that logs are flowing, detection rules are active, and coverage spans your critical systems.
The three things you need to demonstrate:
Log source coverage. Every critical system should be feeding your central log repository such as a SIEM. The minimum set for a cloud-centric organisation: Azure Activity Logs, Entra ID Sign-in Logs, Microsoft Defender for Endpoint events, and network flow logs. Run the following query weekly and store the output — it proves your log pipeline was healthy during the audit period:
KQL
// CC7.1 Evidence: Log source health check
// A healthy environment shows consistent event counts across all sources
let Sources = datatable(Source: string) [
"AzureActivity",
"SigninLogs",
"AuditLogs",
"SecurityEvent",
"DeviceLogonEvents"
];
Sources
| extend EventCount = toscalar(
union AzureActivity, SigninLogs, AuditLogs, SecurityEvent, DeviceLogonEvents
| where TimeGenerated > ago(24h)
| where Type == Source
| count
)
| project Source, EventCount,
Status = iff(EventCount > 100, "✅ Active", "⚠️ Low Volume")// CC7.1 Evidence: Log source health check
// A healthy environment shows consistent event counts across all sources
let Sources = datatable(Source: string) [
"AzureActivity",
"SigninLogs",
"AuditLogs",
"SecurityEvent",
"DeviceLogonEvents"
];
Sources
| extend EventCount = toscalar(
union AzureActivity, SigninLogs, AuditLogs, SecurityEvent, DeviceLogonEvents
| where TimeGenerated > ago(24h)
| where Type == Source
| count
)
| project Source, EventCount,
Status = iff(EventCount > 100, "✅ Active", "⚠️ Low Volume")Detection rule inventory. Export your Sentinel analytics rules (az sentinel alert-rule list --output json). Store this export monthly. The auditor wants to see that you have detection coverage, not every possible threat, but a defensible set mapped to your risk register. If R-AD-002 (over-privileged admin) is in your register, you should have an alert for unusual Global Admin activations.
Alert tuning log. A SIEM generating 500 alerts a day with 90% false positives is not evidence of monitoring, it's evidence of noise. Keep a log of alert rule modifications: what you changed, why, and what the before/after false positive rate was.
CC7.2 — Incident Response
This is the control that trips up most organisations. Having an incident response plan in a SharePoint folder is not the same as having a tested, operational IR capability.
What auditors look for:
Documented IR playbooks, version controlled. Not a Word document last touched in 2022 e.g. a document in Git with a commit history showing it's maintained.
Incident records with timestamps. Every security incident even low-severity ones like a locked-out admin account or a phishing simulation click should be logged in a ticketing system with detection time, triage time, containment time, resolution time, and a brief root cause note. These records are your MTTD and MTTR evidence.
MTTD/MTTR metrics. This query calculates your incident response performance over the last 90 days directly from Defender:
// CC7.2 Evidence: Incident response metrics
// MTTD = time from incident occurrence to first security alert
// MTTR = time from first alert to final closure/mitigation
AlertInfo
| where Timestamp > ago(90d)
| extend
// Calculating differences based on Defender's schema fields
MTTD = datetime_diff('minute', Timestamp, FirstEventTime),
MTTR = datetime_diff('hour', LastEventTime, Timestamp)
| summarize
TotalIncidents = count(),
AvgMTTD_mins = avg(MTTD),
AvgMTTR_hours = avg(MTTR),
P95_MTTR_hours = percentile(MTTR, 95),
CriticalCount = countif(Severity == "High")
by bin(Timestamp, 30d)
| order by Timestamp desc// CC7.2 Evidence: Incident response metrics
// MTTD = time from incident occurrence to first security alert
// MTTR = time from first alert to final closure/mitigation
AlertInfo
| where Timestamp > ago(90d)
| extend
// Calculating differences based on Defender's schema fields
MTTD = datetime_diff('minute', Timestamp, FirstEventTime),
MTTR = datetime_diff('hour', LastEventTime, Timestamp)
| summarize
TotalIncidents = count(),
AvgMTTD_mins = avg(MTTD),
AvgMTTR_hours = avg(MTTR),
P95_MTTR_hours = percentile(MTTR, 95),
CriticalCount = countif(Severity == "High")
by bin(Timestamp, 30d)
| order by Timestamp descRun this monthly and document the trend. Auditors appreciate improving metrics, they show a maturing program. Stagnant or worsening metrics with no documented remediation plan are a red flag.
CC9 — Risk Mitigation — Vendor Risk
CC9.2 — Third-Party Risk Management
Every vendor with access to your environment, your data, or your infrastructure is a probable risk scenario. Auditors want to see a structured program, not a spreadsheet someone updates when they remember.
Evidence required: a vendor inventory with risk tier classification (critical/high/medium/low based on data access and system access), annual security questionnaires or SOC 2 reports collected from high-tier vendors, and access reviews confirming vendor accounts are deprovisioned when relationships end.
The practical standard: any vendor with direct system access or access to personal data must provide a SOC 2 Type 2 report, an equivalent certification (like ISO 27001), or complete your annual security questionnaire. Store these in a version-controlled repository with the collection date. Auditors will sample-check whether your highest-risk vendors have current assessments.
3.2 Risk Register → Control Mapping: Two Real Examples
This is where Chapter 2 connects to Chapter 3. Every High or Critical risk in your register should map to one or more TSC controls, with evidence that those controls are operating.
Example 1: Over-Provisioned Admin Roles (R-AD-002)
In Chapter 2, we scored this risk at Inherent 20 (Critical), Residual 12 (High) after partial PIM deployment.
The TSC controls that govern this risk:
CC6.1 requires that access is restricted to authorised users. Your evidence: Conditional Access policy exports, quarterly access review completion logs, and PIM activation audit trails showing no permanent Global Admin assignments.
CC6.3 requires least privilege. Your evidence: the stale accounts script output showing no accounts with 90+ days of inactivity in privileged roles, PIM activation justification logs, and a record of any role assignments removed during the audit period.
CC7.1 requires monitoring. Your evidence: a Sentinel analytics rule that fires on unexpected Global Admin role activation, with alert history showing it was active throughout the audit window.
If your residual risk is still High (as in our example), you must document the gap and show it's actively being mitigated. An acknowledged, tracked gap with a named owner and due date is acceptable. An undocumented gap is a finding.
Example 2: Untested Backup & Recovery (R-BCP-001)
Inherent 16 (High), Residual 8 (Medium) after establishing backup schedules without testing restoration.
CC7.2 (Incident Response) governs your ability to recover. Your evidence: backup restoration test logs showing that you tested recovery for critical systems during the audit period, with documented RTO and RPO outcomes. A test that shows you missed your RTO target but that you documented it and have a remediation plan is better than no test at all.
CC1.2 (Board Oversight) requires that material risks reach the board. A BCP gap of this nature should appear in your risk register summary presented to the audit committee. The meeting minutes referencing this risk are your evidence.
What most organisations miss: the restoration test must actually validate data integrity, not just confirm that files were copied. If you restore a database and it won't start or restores to a state 6 hours older than your RPO allows, that's a failed test. Document it honestly and track the remediation.
3.3 Building Your Evidence Repository
An audit evidence package is only as good as its organisation. Auditors work under time pressure, if they can't find what they need, they assume it doesn't exist.
Structure your evidence folder like this:
/evidence
/CC6.1_logical_access
CA_Policies_20250101.json
CA_Policies_20250401.json
MFA_Enrollment_Report_Q1.csv
Access_Review_Completion_Q1.pdf
/CC6.3_least_privilege
RBAC_Assignments_Q1.csv
Stale_Accounts_Review_20250115.csv
PIM_Activations_Q1.csv
/CC7.1_monitoring
Log_Source_Health_Jan2025.json
Detection_Rules_Export_20250101.json
/CC7.2_incident_response
IR_Playbook_v3.2.md (commit history in Git)
Incidents_Q1_2025.csv
MTTD_MTTR_Q1.csv
/CC9.2_vendor_risk
Vendor_Inventory_2025.xlsx
Vendor_SOC2_Reports/
EVIDENCE_INDEX.md/evidence
/CC6.1_logical_access
CA_Policies_20250101.json
CA_Policies_20250401.json
MFA_Enrollment_Report_Q1.csv
Access_Review_Completion_Q1.pdf
/CC6.3_least_privilege
RBAC_Assignments_Q1.csv
Stale_Accounts_Review_20250115.csv
PIM_Activations_Q1.csv
/CC7.1_monitoring
Log_Source_Health_Jan2025.json
Detection_Rules_Export_20250101.json
/CC7.2_incident_response
IR_Playbook_v3.2.md (commit history in Git)
Incidents_Q1_2025.csv
MTTD_MTTR_Q1.csv
/CC9.2_vendor_risk
Vendor_Inventory_2025.xlsx
Vendor_SOC2_Reports/
EVIDENCE_INDEX.mdThe EVIDENCE_INDEX.md is a simple table mapping each control to its evidence files and collection dates. Auditors love it because it tells them exactly where to look. It also forces you to identify gaps — if you're writing the index and a control row is empty; you know what you need to collect before the audit starts.
3.4 Continuous Compliance: Stop Scrambling Before Audits
The biggest mistake organisations make with SOC 2 Type 2 is treating evidence collection as a pre-audit sprint. You end up reconstructing three months of history in two weeks, and the evidence shows it, inconsistent timestamps, gaps in coverage, artefacts that look like they were generated rather than collected.
Build a simple automation pipeline instead. A monthly GitHub Actions workflow that:
- Exports Conditional Access policies to your evidence repository
- Pulls MFA enrollment reports via Graph API
- Runs the stale accounts script and stores output
- Queries Sentinel for MTTD/MTTR metrics and exports the results
- Commits everything to a version-controlled evidence repository with a dated commit message
Each commit is itself evidence, it proves the artefact was collected on a specific date, not fabricated later. The commit history over 12 months is your audit trail.
For real-time visibility, a simple Power BI or Grafana dashboard tracking four metrics covers most of what auditors care about: MFA coverage percentage (target: 100%), privileged accounts without PIM (target: 0), log source health (all sources active), and open High/Critical risks with approaching due dates. When that dashboard is green, you're audit-ready. When it's amber, you know what to fix before the window closes.
Practitioner Checklist: SOC 2 Type 2 Readiness
✅ Risk Register Connected to Controls: Every High/Critical risk maps to a named TSC control. Each risk has a named owner and documented residual risk. Quarterly review cycle is evidenced in the register
✅ CC6 — Access Controls: Conditional Access policies exported monthly with timestamps. MFA enrollment at 100% (or gaps documented with compensating controls). Quarterly access review completion logs in Identity Governance. PIM enforced for all privileged roles; no permanent Global Admin assignments. Stale account review completed within the last 90 days
✅ CC7 — Monitoring & Incident Response: All critical log sources validated as active (query output stored). Detection rule inventory exported and dated. Incident records with MTTD/MTTR data for the full audit period. IR playbook version-controlled with commit history. Backup restoration test completed and documented
✅ CC9 — Vendor Risk: Vendor inventory with risk tier classification. High-tier vendors have current SOC 2 reports or completed questionnaires. Vendor access reviewed and deprovisioned upon contract end
✅ Evidence Repository: Folder structure matches TSC controls. All artefacts timestamped and versioned. Evidence index generated and current. Monthly automated collection pipeline running
🔜 What's Next?
In Chapter 4: Azure Fundamentals for Red Teams, we shift from governance back to the attack side, modern authentication flows, token abuse, and how adversaries leverage Azure AD misconfigurations to move laterally through cloud environments. Everything we've built in Chapters 1–3 will show up in Chapter 4 as the attack surface.
Found this useful? Clap 👏, follow @thedarklight6996, and drop a comment: What's the hardest control to evidence in your SOC 2 program?
Series:_ Attack → Detect → Govern Source: Breached, Detected & Governed — The Complete Edition Next: Chapter 4 — Azure Fundamentals for Red Teams_ (coming soon)