SOC2/Get-SOC2AuditEvidence.ps1
|
<#
.SYNOPSIS Collects SOC 2 audit evidence from Microsoft 365 activity logs. .DESCRIPTION Queries Microsoft Graph audit logs and sign-in logs to collect evidence that demonstrates active monitoring and incident response for SOC 2 compliance. Covers both Security and Confidentiality trust principles. Evidence includes failed sign-in attempts, risky sign-in detections, alert response activity, privileged role changes, sharing events, and DLP policy matches over the last 30 days. All operations are strictly read-only (Graph GET requests only). DISCLAIMER: This tool assists with SOC 2 readiness assessment. It does not constitute a SOC 2 audit or certification. Requires Microsoft Graph connection with: AuditLog.Read.All, SecurityEvents.Read.All, IdentityRiskEvent.Read.All .PARAMETER OutputPath Optional path to export results as CSV. If not specified, results are returned to the pipeline. .PARAMETER EvidenceWindowDays Number of days to look back for evidence. Defaults to 30. .EXAMPLE PS> . .\Common\Connect-Service.ps1 PS> Connect-Service -Service Graph -Scopes 'AuditLog.Read.All','SecurityEvents.Read.All' PS> .\SOC2\Get-SOC2AuditEvidence.ps1 Displays SOC 2 audit evidence summary. .EXAMPLE PS> .\SOC2\Get-SOC2AuditEvidence.ps1 -OutputPath '.\soc2-evidence.csv' -EvidenceWindowDays 60 Exports 60-day evidence window to CSV. .NOTES Author: Daren9m #> [CmdletBinding()] param( [Parameter()] [ValidateNotNullOrEmpty()] [string]$OutputPath, [Parameter()] [ValidateRange(1, 365)] [int]$EvidenceWindowDays = 30 ) $ErrorActionPreference = 'Stop' # Verify Graph connection if (-not (Assert-GraphConnection)) { return } $results = [System.Collections.Generic.List[PSCustomObject]]::new() $startDate = (Get-Date).AddDays(-$EvidenceWindowDays).ToString('yyyy-MM-ddTHH:mm:ssZ') # Note: Evidence queries use $top=100 as a representative sample. For high-volume # tenants this may not capture all events. The EventCount reflects the sample size. # For full-population evidence, export via Microsoft Purview Audit (Premium) or # the continuous monitoring automation described in the Daily Monitoring Strategy. # Helper to add evidence summary function Add-EvidenceSummary { param( [string]$TrustPrinciple, [string]$TSCReference, [string]$EvidenceId, [string]$EvidenceType, [int]$EventCount, [string]$TimeWindow, [string]$Summary, [string]$Status, [string]$SampleEvents = '' ) $results.Add([PSCustomObject]@{ TrustPrinciple = $TrustPrinciple TSCReference = $TSCReference EvidenceId = $EvidenceId EvidenceType = $EvidenceType EventCount = $EventCount TimeWindow = $TimeWindow Summary = $Summary Status = $Status SampleEvents = $SampleEvents }) } $timeWindowLabel = "Last $EvidenceWindowDays days (since $($startDate.Substring(0,10)))" # ------------------------------------------------------------------ # E-01: Failed Sign-in Attempts (CC7.2 — Security) # ------------------------------------------------------------------ try { Write-Verbose "E-01: Querying failed sign-in events..." $failedSignIns = Invoke-MgGraphRequest -Method GET ` -Uri "/v1.0/auditLogs/signIns?`$filter=createdDateTime ge $startDate and status/errorCode ne 0&`$top=100&`$orderby=createdDateTime desc" ` -ErrorAction Stop $events = if ($failedSignIns -and $failedSignIns['value']) { @($failedSignIns['value']) } else { @() } $sampleCapped = $events.Count -ge 100 $summary = if ($events.Count -eq 0) { 'No failed sign-in attempts detected' } else { $topUsers = @($events | Group-Object -Property { $_['userPrincipalName'] } | Sort-Object -Property Count -Descending | Select-Object -First 3) $topUserSummary = ($topUsers | ForEach-Object { "$($_.Name): $($_.Count)" }) -join '; ' $capNote = if ($sampleCapped) { ' (sample capped at 100; actual count may be higher)' } else { '' } "Top users with failures: $topUserSummary$capNote" } $sampleData = ($events | Select-Object -First 3 | ForEach-Object { "$($_['createdDateTime']): $($_['userPrincipalName']) - $($_['status']['failureReason'])" }) -join ' | ' Add-EvidenceSummary -TrustPrinciple 'Security' -TSCReference 'CC7.2' -EvidenceId 'E-01' ` -EvidenceType 'Failed Sign-in Attempts' -EventCount $events.Count ` -TimeWindow $timeWindowLabel -Summary $summary -Status 'Collected' ` -SampleEvents $sampleData } catch { Write-Warning "E-01: Failed to query sign-in logs: $_" Add-EvidenceSummary -TrustPrinciple 'Security' -TSCReference 'CC7.2' -EvidenceId 'E-01' ` -EvidenceType 'Failed Sign-in Attempts' -EventCount 0 ` -TimeWindow $timeWindowLabel -Summary "Error: $_" -Status 'Error' } # ------------------------------------------------------------------ # E-02: Risky Sign-in Detections (CC7.2 — Security) # ------------------------------------------------------------------ try { Write-Verbose "E-02: Querying risky sign-in detections..." $riskDetections = Invoke-MgGraphRequest -Method GET ` -Uri "/v1.0/identityProtection/riskDetections?`$filter=activityDateTime ge $startDate&`$top=100&`$orderby=activityDateTime desc" ` -ErrorAction Stop $events = if ($riskDetections -and $riskDetections['value']) { @($riskDetections['value']) } else { @() } $sampleCapped = $events.Count -ge 100 $summary = if ($events.Count -eq 0) { 'No risky sign-in detections in the evidence window' } else { $riskTypes = @($events | Group-Object -Property { $_['riskEventType'] } | Sort-Object -Property Count -Descending | Select-Object -First 3) $riskSummary = ($riskTypes | ForEach-Object { "$($_.Name): $($_.Count)" }) -join '; ' $capNote = if ($sampleCapped) { ' (sample capped at 100)' } else { '' } "Risk types detected: $riskSummary$capNote" } $sampleData = ($events | Select-Object -First 3 | ForEach-Object { "$($_['activityDateTime']): $($_['userPrincipalName']) - $($_['riskEventType']) ($($_['riskLevel']))" }) -join ' | ' Add-EvidenceSummary -TrustPrinciple 'Security' -TSCReference 'CC7.2' -EvidenceId 'E-02' ` -EvidenceType 'Risky Sign-in Detections' -EventCount $events.Count ` -TimeWindow $timeWindowLabel -Summary $summary -Status 'Collected' ` -SampleEvents $sampleData } catch { Write-Warning "E-02: Failed to query risk detections (may require P2 license): $_" $status = if ("$_" -match 'Forbidden|403|license') { 'NotLicensed' } else { 'Error' } Add-EvidenceSummary -TrustPrinciple 'Security' -TSCReference 'CC7.2' -EvidenceId 'E-02' ` -EvidenceType 'Risky Sign-in Detections' -EventCount 0 ` -TimeWindow $timeWindowLabel -Summary "Unavailable: $_ (Requires Entra ID P2 license)" -Status $status } # ------------------------------------------------------------------ # E-04: Alert Response Activity (CC7.3 — Security) # ------------------------------------------------------------------ try { Write-Verbose "E-04: Querying security alert activity..." $alerts = Invoke-MgGraphRequest -Method GET ` -Uri "/v1.0/security/alerts_v2?`$top=100&`$orderby=createdDateTime desc" ` -ErrorAction Stop $events = if ($alerts -and $alerts['value']) { @($alerts['value']) } else { @() } $sampleCapped = $events.Count -ge 100 $newAlerts = @($events | Where-Object { $_['status'] -eq 'new' }) $resolvedAlerts = @($events | Where-Object { $_['status'] -eq 'resolved' }) $inProgressAlerts = @($events | Where-Object { $_['status'] -eq 'inProgress' }) $capNote = if ($sampleCapped) { ' (sample capped at 100)' } else { '' } $summary = if ($events.Count -eq 0) { 'No security alerts generated (clean environment or no Defender configured)' } else { "Total: $($events.Count); New: $($newAlerts.Count); In Progress: $($inProgressAlerts.Count); Resolved: $($resolvedAlerts.Count)$capNote" } $sampleData = ($events | Select-Object -First 3 | ForEach-Object { "$($_['createdDateTime']): $($_['title']) [$($_['status'])]" }) -join ' | ' Add-EvidenceSummary -TrustPrinciple 'Security' -TSCReference 'CC7.3' -EvidenceId 'E-04' ` -EvidenceType 'Alert Response Activity' -EventCount $events.Count ` -TimeWindow $timeWindowLabel -Summary $summary -Status 'Collected' ` -SampleEvents $sampleData } catch { Write-Warning "E-04: Failed to query security alerts: $_" Add-EvidenceSummary -TrustPrinciple 'Security' -TSCReference 'CC7.3' -EvidenceId 'E-04' ` -EvidenceType 'Alert Response Activity' -EventCount 0 ` -TimeWindow $timeWindowLabel -Summary "Error: $_" -Status 'Error' } # ------------------------------------------------------------------ # E-08: Privileged Role Changes (CC6.3 — Security) # ------------------------------------------------------------------ try { Write-Verbose "E-08: Querying privileged role change events..." $roleAudits = Invoke-MgGraphRequest -Method GET ` -Uri "/v1.0/auditLogs/directoryAudits?`$filter=activityDateTime ge $startDate and category eq 'RoleManagement'&`$top=100&`$orderby=activityDateTime desc" ` -ErrorAction Stop $events = if ($roleAudits -and $roleAudits['value']) { @($roleAudits['value']) } else { @() } $sampleCapped = $events.Count -ge 100 $summary = if ($events.Count -eq 0) { 'No privileged role changes in the evidence window' } else { $activities = @($events | Group-Object -Property { $_['activityDisplayName'] } | Sort-Object -Property Count -Descending | Select-Object -First 3) $actSummary = ($activities | ForEach-Object { "$($_.Name): $($_.Count)" }) -join '; ' $capNote = if ($sampleCapped) { ' (sample capped at 100)' } else { '' } "Role management activities: $actSummary$capNote" } $sampleData = ($events | Select-Object -First 3 | ForEach-Object { $actor = if ($_['initiatedBy']['user']) { $_['initiatedBy']['user']['userPrincipalName'] } else { 'System' } "$($_['activityDateTime']): $($_['activityDisplayName']) by $actor" }) -join ' | ' Add-EvidenceSummary -TrustPrinciple 'Security' -TSCReference 'CC6.3' -EvidenceId 'E-08' ` -EvidenceType 'Privileged Role Changes' -EventCount $events.Count ` -TimeWindow $timeWindowLabel -Summary $summary -Status 'Collected' ` -SampleEvents $sampleData } catch { Write-Warning "E-08: Failed to query role change audits: $_" Add-EvidenceSummary -TrustPrinciple 'Security' -TSCReference 'CC6.3' -EvidenceId 'E-08' ` -EvidenceType 'Privileged Role Changes' -EventCount 0 ` -TimeWindow $timeWindowLabel -Summary "Error: $_" -Status 'Error' } # ------------------------------------------------------------------ # E-05: Sharing Events Detected (C1.1 — Confidentiality) # ------------------------------------------------------------------ try { Write-Verbose "E-05: Querying SharePoint sharing events via Unified Audit Log..." $sharingEvents = $null $sharingCount = 0 # Try UAL via EXO cmdlet try { $null = Get-Command -Name Search-UnifiedAuditLog -ErrorAction Stop $endDate = Get-Date $ualStartDate = (Get-Date).AddDays(-$EvidenceWindowDays) $sharingEvents = @(Search-UnifiedAuditLog -StartDate $ualStartDate -EndDate $endDate ` -Operations 'SharingSet','SharingInvitationCreated' -ResultSize 100 -ErrorAction Stop) $sharingCount = $sharingEvents.Count } catch { Write-Verbose "UAL not available via EXO; skipping sharing event evidence." } $summary = if ($null -eq $sharingEvents) { 'Unable to query (requires EXO connection for Unified Audit Log)' } elseif ($sharingCount -eq 0) { 'No sharing events detected in the evidence window' } else { "$sharingCount sharing events detected" } $status = if ($null -eq $sharingEvents) { 'Review' } else { 'Collected' } Add-EvidenceSummary -TrustPrinciple 'Confidentiality' -TSCReference 'C1.1' -EvidenceId 'E-05' ` -EvidenceType 'Sharing Events Detected' -EventCount $sharingCount ` -TimeWindow $timeWindowLabel -Summary $summary -Status $status } catch { Write-Warning "E-05: Failed to query sharing events: $_" Add-EvidenceSummary -TrustPrinciple 'Confidentiality' -TSCReference 'C1.1' -EvidenceId 'E-05' ` -EvidenceType 'Sharing Events Detected' -EventCount 0 ` -TimeWindow $timeWindowLabel -Summary "Error: $_" -Status 'Error' } # ------------------------------------------------------------------ # E-07: DLP Policy Matches (C1.2 — Confidentiality) # ------------------------------------------------------------------ try { Write-Verbose "E-07: Querying DLP policy match events via Unified Audit Log..." $dlpEvents = $null $dlpCount = 0 # Try UAL via EXO cmdlet try { $null = Get-Command -Name Search-UnifiedAuditLog -ErrorAction Stop $endDate = Get-Date $ualStartDate = (Get-Date).AddDays(-$EvidenceWindowDays) $dlpEvents = @(Search-UnifiedAuditLog -StartDate $ualStartDate -EndDate $endDate ` -Operations 'DlpRuleMatch' -ResultSize 100 -ErrorAction Stop) $dlpCount = $dlpEvents.Count } catch { Write-Verbose "UAL not available via EXO; skipping DLP event evidence." } $summary = if ($null -eq $dlpEvents) { 'Unable to query (requires EXO connection for Unified Audit Log)' } elseif ($dlpCount -eq 0) { 'No DLP policy matches in the evidence window' } else { "$dlpCount DLP policy match events detected" } $status = if ($null -eq $dlpEvents) { 'Review' } else { 'Collected' } Add-EvidenceSummary -TrustPrinciple 'Confidentiality' -TSCReference 'C1.2' -EvidenceId 'E-07' ` -EvidenceType 'DLP Policy Matches' -EventCount $dlpCount ` -TimeWindow $timeWindowLabel -Summary $summary -Status $status } catch { Write-Warning "E-07: Failed to query DLP events: $_" Add-EvidenceSummary -TrustPrinciple 'Confidentiality' -TSCReference 'C1.2' -EvidenceId 'E-07' ` -EvidenceType 'DLP Policy Matches' -EventCount 0 ` -TimeWindow $timeWindowLabel -Summary "Error: $_" -Status 'Error' } # ------------------------------------------------------------------ # Output results # ------------------------------------------------------------------ if ($results.Count -eq 0) { Write-Warning "No SOC 2 audit evidence was collected." return } Write-Verbose "SOC 2 evidence items collected: $($results.Count)" if ($OutputPath) { $results | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8 Write-Output "Exported $($results.Count) SOC 2 evidence items to $OutputPath" } else { Write-Output $results } |