Skip to content

Commit

Permalink
Merge pull request #1130 from JohnDuprey/dev
Browse files Browse the repository at this point in the history
Audit logs
  • Loading branch information
KelvinTegelaar authored Oct 2, 2024
2 parents 05f28ef + a915505 commit afd346e
Show file tree
Hide file tree
Showing 13 changed files with 232 additions and 100 deletions.
3 changes: 2 additions & 1 deletion CIPPTimers.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"Cron": "0 */15 * * * *",
"Priority": 2,
"RunOnProcessor": true,
"PreferredProcessor": "auditlog"
"PreferredProcessor": "auditlog",
"IsSystem": true
},
{
"Command": "Start-WebhookOrchestrator",
Expand Down
6 changes: 4 additions & 2 deletions Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuotaUsed.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ function Get-CIPPAlertQuotaUsed {
}
$OverQuota = $AlertData | ForEach-Object {
if ($_.StorageUsedInBytes -eq 0 -or $_.prohibitSendReceiveQuotaInBytes -eq 0) { return }
$PercentLeft = [math]::round(($_.storageUsedInBytes / $_.prohibitSendReceiveQuotaInBytes) * 100)
try {
$PercentLeft = [math]::round(($_.storageUsedInBytes / $_.prohibitSendReceiveQuotaInBytes) * 100)
} catch { $PercentLeft = 100 }
try {
if ([int]$InputValue -gt 0) {
$Value = [int]$InputValue
Expand All @@ -34,4 +36,4 @@ function Get-CIPPAlertQuotaUsed {

}
Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $OverQuota
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
function Get-CippAuditLogSearchResults {
<#
.SYNOPSIS
Get the results of an audit log search
.DESCRIPTION
Get the results of an audit log search from the Graph API
.PARAMETER TenantFilter
The tenant to filter on.
.PARAMETER QueryId
The ID of the query to get the results for.
#>
param (
[Parameter(Mandatory = $true)]
[string]$TenantFilter,
[Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
[Alias('id')]
[string]$QueryId
)

process {
New-GraphGetRequest -uri ('https://graph.microsoft.com/beta/security/auditLog/queries/{0}/records?$top=999' -f $QueryId) -AsApp $true -tenantid $TenantFilter
}
}
23 changes: 23 additions & 0 deletions Modules/CIPPCore/Public/AuditLogs/Get-CippAuditLogSearches.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
function Get-CippAuditLogSearches {
<#
.SYNOPSIS
Get the available audit log searches
.DESCRIPTION
Query the Graph API for available audit log searches.
.PARAMETER TenantFilter
The tenant to filter on.
#>
param (
[Parameter(Mandatory = $true)]
[string]$TenantFilter,
[Parameter()]
[switch]$ReadyToProcess
)
$Queries = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/security/auditLog/queries' -AsApp $true -tenantid $TenantFilter
if ($ReadyToProcess.IsPresent) {
$AuditLogSearchesTable = Get-CippTable -TableName 'AuditLogSearches'
$PendingQueries = Get-CIPPAzDataTableEntity @AuditLogSearchesTable -Filter "Tenant eq '$TenantFilter' and CippStatus eq 'Pending'"
$Queries = $Queries | Where-Object { $PendingQueries.RowKey -contains $_.id -and $_.status -eq 'succeeded' }
}
return $Queries
}
19 changes: 19 additions & 0 deletions Modules/CIPPCore/Public/AuditLogs/Get-CippLastAuditLogSearch.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
function Get-CippLastAuditLogSearch {
<#
.SYNOPSIS
Get the last audit log search
.DESCRIPTION
Query the Graph API for the last audit log search.
.PARAMETER TenantFilter
The tenant to filter on.
#>
param (
[Parameter(Mandatory = $true)]
[string]$TenantFilter
)

$Table = Get-CIPPTable -TableName AuditLogSearches
$LastHour = (Get-Date).AddHours(-1).ToString('yyyy-MM-ddTHH:mm:ssZ')
$LastSearch = Get-AzDataTableEntity @Table -Filter "Tenant eq '$TenantFilter' and Timestamp ge datetime'$LastHour'" | Sort-Object Timestamp -Descending | Select-Object -First 1
return $LastSearch
}
24 changes: 22 additions & 2 deletions Modules/CIPPCore/Public/AuditLogs/New-CippAuditLogSearch.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ function New-CippAuditLogSearch {
The object IDs to filter on.
.PARAMETER AdministrativeUnitFilters
The administrative units to filter on.
.PARAMETER ProcessLogs
Store the search in the CIPP AuditLogSearches table for alert processing.
#>
[CmdletBinding(SupportsShouldProcess = $true)]
param(
Expand Down Expand Up @@ -117,7 +119,9 @@ function New-CippAuditLogSearch {
[Parameter()]
[string[]]$ObjectIdFilters,
[Parameter()]
[string[]]$AdministrativeUnitFilters
[string[]]$AdministrativeUnitFilters,
[Parameter()]
[switch]$ProcessLogs
)

$SearchParams = @{
Expand Down Expand Up @@ -151,6 +155,22 @@ function New-CippAuditLogSearch {
}

if ($PSCmdlet.ShouldProcess('Create a new audit log search for tenant ' + $TenantFilter)) {
New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/security/auditLog/queries' -body ($SearchParams | ConvertTo-Json -Compress) -tenantid $TenantFilter -AsApp $true
$Query = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/security/auditLog/queries' -body ($SearchParams | ConvertTo-Json -Compress) -tenantid $TenantFilter -AsApp $true

if ($ProcessLogs.IsPresent -and $Query.id) {
$Entity = [PSCustomObject]@{
PartitionKey = [string]'Search'
RowKey = [string]$Query.id
Tenant = [string]$TenantFilter
DisplayName = [string]$DisplayName
StartTime = [datetime]$StartTime.ToUniversalTime()
EndTime = [datetime]$EndTime.ToUniversalTime()
Query = [string]($Query | ConvertTo-Json -Compress)
CippStatus = [string]'Pending'
}
$Table = Get-CIPPTable -TableName 'AuditLogSearches'
Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force | Out-Null
}
return $Query
}
}
Original file line number Diff line number Diff line change
@@ -1,78 +1,60 @@
function Push-AuditLogTenant {
Param($Item)

# Get Table contexts
$AuditBundleTable = Get-CippTable -tablename 'AuditLogBundles'
$SchedulerConfig = Get-CippTable -TableName 'SchedulerConfig'
$WebhookTable = Get-CippTable -tablename 'webhookTable'
$ConfigTable = Get-CippTable -TableName 'WebhookRules'
#$Tenant = Get-Tenants -TenantFilter $Item.customerId -IncludeErrors
$TenantFilter = $Item.TenantFilter

Write-Information "Audit Logs: Processing $($TenantFilter)"
# Query CIPPURL for linking
$CIPPURL = Get-CIPPAzDataTableEntity @SchedulerConfig -Filter "PartitionKey eq 'webhookcreation'" | Select-Object -First 1 -ExpandProperty CIPPURL

# Get all webhooks for the tenant
$Webhooks = Get-CIPPAzDataTableEntity @WebhookTable -Filter "PartitionKey eq '$($Item.TenantFilter)' and Version eq '3'" | Where-Object { $_.Resource -match '^Audit' }

# Get webhook rules
$ConfigEntries = Get-CIPPAzDataTableEntity @ConfigTable
$LogSearchesTable = Get-CippTable -TableName 'AuditLogSearches'

# Date filter for existing bundles
$LastHour = (Get-Date).AddHours(-1).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss')

$NewBundles = [System.Collections.Generic.List[object]]::new()
foreach ($Webhook in $Webhooks) {
# only process webhooks that are configured in the webhookrules table
$Configuration = $ConfigEntries | Where-Object { ($_.Tenants -match $TenantFilter -or $_.Tenants -match 'AllTenants') }
if ($Configuration.Type -notcontains $Webhook.Resource) {
continue
}

$TenantFilter = $Webhook.PartitionKey
$LogType = $Webhook.Resource
Write-Information "Querying for $LogType on $TenantFilter"
$ContentBundleQuery = @{
TenantFilter = $TenantFilter
ContentType = $LogType
StartTime = $Item.StartTime
EndTime = $Item.EndTime
}
$Configuration = $ConfigEntries | Where-Object { ($_.Tenants -match $TenantFilter -or $_.Tenants -match 'AllTenants') }
if ($Configuration) {
try {
$LogBundles = Get-CIPPAuditLogContentBundles @ContentBundleQuery
$ExistingBundles = Get-CIPPAzDataTableEntity @AuditBundleTable -Filter "PartitionKey eq '$($Item.TenantFilter)' and ContentType eq '$LogType' and Timestamp ge datetime'$($LastHour)'"

foreach ($Bundle in $LogBundles) {
if ($ExistingBundles.RowKey -notcontains $Bundle.contentId) {
$NewBundles.Add([PSCustomObject]@{
PartitionKey = $TenantFilter
RowKey = $Bundle.contentId
DefaultDomainName = $TenantFilter
ContentType = $Bundle.contentType
ContentUri = $Bundle.contentUri
ContentCreated = $Bundle.contentCreated
ContentExpiration = $Bundle.contentExpiration
CIPPURL = [string]$CIPPURL
ProcessingStatus = 'Pending'
MatchedRules = ''
MatchedLogs = 0
})
$LogSearches = Get-CippAuditLogSearches -TenantFilter $TenantFilter -ReadyToProcess
Write-Information ('Audit Logs: Found {0} searches, begin processing' -f $LogSearches.Count)
foreach ($Search in $LogSearches) {
$SearchEntity = Get-CIPPAzDataTableEntity @LogSearchesTable -Filter "Tenant eq '$($TenantFilter)' and RowKey eq '$($Search.id)'"
$SearchEntity.CippStatus = 'Processing'
Add-CIPPAzDataTableEntity @LogSearchesTable -Entity $SearchEntity -Force
try {
# Test the audit log rules against the search results
$AuditLogTest = Test-CIPPAuditLogRules -TenantFilter $TenantFilter -SearchId $Search.id

$SearchEntity.CippStatus = 'Completed'
$MatchedRules = [string](ConvertTo-Json -Compress -InputObject $AuditLogTest.MatchedRules)
$SearchEntity | Add-Member -MemberType NoteProperty -Name MatchedRules -Value $MatchedRules
$SearchEntity | Add-Member -MemberType NoteProperty -Name MatchedLogs -Value $AuditLogTest.MatchedLogs
$SearchEntity | Add-Member -MemberType NoteProperty -Name TotalLogs -Value $AuditLogTest.TotalLogs
} catch {
$SearchEntity.CippStatus = 'Failed'
Write-Information "Error processing audit log rules: $($_.Exception.Message)"
$Exception = [string](ConvertTo-Json -Compress -InputObject (Get-CippException -Exception $_))
$SearchEntity | Add-Member -MemberType NoteProperty -Name Error -Value $Exception
}
Add-CIPPAzDataTableEntity @LogSearchesTable -Entity $SearchEntity -Force
$DataToProcess = ($AuditLogTest).DataToProcess
Write-Information "Audit Logs: Data to process found: $($DataToProcess.count) items"
if ($DataToProcess) {
foreach ($AuditLog in $DataToProcess) {
Write-Information "Processing $($AuditLog.operation)"
$Webhook = @{
Data = $AuditLog
CIPPURL = [string]$CIPPURL
TenantFilter = $TenantFilter
}
Invoke-CippWebhookProcessing @Webhook
}
}
}
} catch {
Write-Information "Could not get audit log content bundles for $TenantFilter - $LogType, $($_.Exception.Message)"
}
}

if (($NewBundles | Measure-Object).Count -gt 0) {
Add-CIPPAzDataTableEntity @AuditBundleTable -Entity $NewBundles -Force
Write-Information ($NewBundles | ConvertTo-Json -Depth 5 -Compress)

$Batch = $NewBundles | Select-Object @{Name = 'ContentId'; Expression = { $_.RowKey } }, @{Name = 'TenantFilter'; Expression = { $_.PartitionKey } }, @{Name = 'FunctionName'; Expression = { 'AuditLogBundleProcessing' } }
$InputObject = [PSCustomObject]@{
OrchestratorName = 'AuditLogs'
Batch = @($Batch)
SkipLog = $true
Write-Information ( 'Audit Logs: Error {0} line {1} - {2}' -f $_.InvocationInfo.ScriptName, $_.InvocationInfo.ScriptLineNumber, $_.Exception.Message)
}
$InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress)
Write-Host "Started orchestration with ID = '$InstanceId'"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Function Invoke-AddSite {
.FUNCTIONALITY
Entrypoint
.ROLE
Teams.Group.ReadWrite
Sharepoint.Site.ReadWrite
#>
[CmdletBinding()]
param($Request, $TriggerMetadata)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Function Invoke-AddSiteBulk {
.FUNCTIONALITY
Entrypoint
.ROLE
Teams.Group.ReadWrite
Sharepoint.Site.ReadWrite
#>
[CmdletBinding()]
param($Request, $TriggerMetadata)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
function Invoke-ListSharepointAdminUrl {
<#
.FUNCTIONALITY
Entrypoint
.ROLE
CIPP.Core.Read
#>
[CmdletBinding()]
param(
$Request,
$TriggerMetadata
)

if ($Request.Query.TenantFilter) {
$TenantFilter = $Request.Query.TenantFilter

$Tenant = Get-Tenants -TenantFilter $TenantFilter

if ($Tenant.SharepointAdminUrl) {
$AdminUrl = $Tenant.SharepointAdminUrl
} else {
$tenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -asApp $true -tenantid $TenantFilter).id.Split('.')[0]
$AdminUrl = "https://$($tenantName)-admin.sharepoint.com"
$Tenant | Add-Member -MemberType NoteProperty -Name SharepointAdminUrl -Value $AdminUrl
$Table = Get-CIPPTable -TableName 'Tenants'
Add-CIPPAzDataTableEntity @Table -Entity $Tenant -Force
}

if ($Request.Query.ReturnUrl) {
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = [HttpStatusCode]::OK
Body = @{
AdminUrl = $AdminUrl
}
})
} else {
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = [HttpStatusCode]::Found
Headers = @{
Location = $AdminUrl
}
})
}
} else {
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = [HttpStatusCode]::BadRequest
Body = 'TenantFilter is required'
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,41 @@ function Start-AuditLogOrchestrator {
[CmdletBinding(SupportsShouldProcess = $true)]
param()
try {
$webhookTable = Get-CIPPTable -tablename webhookTable
$Webhooks = Get-CIPPAzDataTableEntity @webhookTable -Filter "Version eq '3'" | Where-Object { $_.Resource -match '^Audit' -and $_.Status -ne 'Disabled' }
if (($Webhooks | Measure-Object).Count -eq 0) {
Write-Information 'No webhook subscriptions found. Exiting.'
return
}

$StartTime = (Get-Date).AddMinutes(-30)
$EndTime = Get-Date
$AuditLogSearchesTable = Get-CIPPTable -TableName 'AuditLogSearches'
$AuditLogSearches = Get-CIPPAzDataTableEntity @AuditLogSearchesTable -Filter "CippStatus eq 'Pending'"
$TenantList = Get-Tenants -IncludeErrors
# Round time down to nearest minute
$Now = Get-Date
$StartTime = ($Now.AddSeconds(-$Now.Seconds)).AddMinutes(-30)
$EndTime = $Now.AddSeconds(-$Now.Seconds)

$Queue = New-CippQueueEntry -Name 'Audit Log Collection' -Reference 'AuditLogCollection' -TotalTasks ($Webhooks | Sort-Object -Property PartitionKey -Unique | Measure-Object).Count
if (($AuditLogSearches | Measure-Object).Count -eq 0) {
Write-Information 'No audit log searches available'
} else {
$Queue = New-CippQueueEntry -Name 'Audit Log Collection' -Reference 'AuditLogCollection' -TotalTasks ($AuditLogSearches).Count
$Batch = $AuditLogSearches | Sort-Object -Property Tenant -Unique | Select-Object @{Name = 'TenantFilter'; Expression = { $_.Tenant } }, @{Name = 'QueueId'; Expression = { $Queue.RowKey } }, @{Name = 'FunctionName'; Expression = { 'AuditLogTenant' } }

$Batch = $Webhooks | Sort-Object -Property PartitionKey -Unique | Select-Object @{Name = 'TenantFilter'; Expression = { $_.PartitionKey } }, @{Name = 'QueueId'; Expression = { $Queue.RowKey } }, @{Name = 'FunctionName'; Expression = { 'AuditLogTenant' } }, @{Name = 'StartTime'; Expression = { $StartTime } }, @{Name = 'EndTime'; Expression = { $EndTime } }
$InputObject = [PSCustomObject]@{
OrchestratorName = 'AuditLogs'
Batch = @($Batch)
SkipLog = $true
$InputObject = [PSCustomObject]@{
OrchestratorName = 'AuditLogs'
Batch = @($Batch)
SkipLog = $true
}
if ($PSCmdlet.ShouldProcess('Start-AuditLogOrchestrator', 'Starting Audit Log Polling')) {
Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress)
}
}
if ($PSCmdlet.ShouldProcess('Start-AuditLogPolling', 'Starting Audit Log Polling')) {
Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress)

Write-Information 'Audit Logs: Creating new searches'
foreach ($Tenant in $TenantList) {
try {
$NewSearch = New-CippAuditLogSearch -TenantFilter $Tenant.defaultDomainName -StartTime $StartTime -EndTime $EndTime -ProcessLogs
Write-Information "Created audit log search $($Tenant.defaultDomainName) - $($NewSearch.displayName)"
} catch {
Write-Information "Error creating audit log search $($Tenant.defaultDomainName) - $($_.Exception.Message)"
}
}
} catch {
Write-LogMessage -API 'Webhooks' -message 'Error processing webhooks' -sev Error -LogData (Get-CippException -Exception $_)
Write-Information ( 'Webhook error {0} line {1} - {2}' -f $_.InvocationInfo.ScriptName, $_.InvocationInfo.ScriptLineNumber, $_.Exception.Message)
Write-LogMessage -API 'Audit Logs' -message 'Error processing audit logs' -sev Error -LogData (Get-CippException -Exception $_)
Write-Information ( 'Audit logs error {0} line {1} - {2}' -f $_.InvocationInfo.ScriptName, $_.InvocationInfo.ScriptLineNumber, $_.Exception.Message)
}
}
Loading

0 comments on commit afd346e

Please sign in to comment.