DscResources/FileSystemAuditRuleEntry/FileSystemAuditRuleEntry.psm1

$resourceRootPath = Split-Path -Path $PSScriptRoot -Parent
$resourceHelperPath = Join-Path -Path $resourceRootPath -ChildPath 'AccessControlResourceHelper'
$resourceHelperPsm1 = Join-Path -Path $resourceHelperPath -ChildPath 'AccessControlResourceHelper.psm1'
Import-Module -Name $resourceHelperPsm1 -Force

try
{
    $importLocalizedDataParams = @{
        BaseDirectory = $resourceHelperPath
        UICulture     = $PSUICulture
        FileName      = 'AccessControlResourceHelper.strings.psd1'
        ErrorAction   = 'Stop'
    }

    $script:localizedData = Import-LocalizedData @importLocalizedDataParams
}
catch
{
    $importLocalizedDataParams.UICulture = 'en-US'
    try
    {
        $script:localizedData = Import-LocalizedData @importLocalizedDataParams
    }
    catch
    {
        throw 'Unable to load localized data'
    }
}

<#
    .SYNOPSIS
        Returns the current state of the resource.
#>

function Get-TargetResource
{
    [CmdletBinding()]
    [OutputType([Hashtable])]
    param
    (
        [Parameter(Mandatory=$true)]
        [System.String]
        $Path,

        [Parameter(Mandatory=$true)]
        [Microsoft.Management.Infrastructure.CimInstance[]]
        $AuditRuleList
    )

    $nameSpace = "root/Microsoft/Windows/DesiredStateConfiguration"
    $cimfileSystemAuditRuleList = New-Object -TypeName 'System.Collections.ObjectModel.Collection`1[Microsoft.Management.Infrastructure.CimInstance]'
    $inputPath = Get-InputPath($Path)

    if (Test-Path -Path $inputPath)
    {
        $currentAcl = Get-Acl -Path $inputPath -Audit

        if ($null -ne $currentAcl)
        {
            $message = $localizedData.AclFound -f $inputPath
            Write-Verbose -Message $message

            foreach ($principal in $AuditRuleList)
            {
                $cimFileSystemAuditRule = New-Object -TypeName 'System.Collections.ObjectModel.Collection`1[Microsoft.Management.Infrastructure.CimInstance]'

                $principalName = $principal.Principal
                $forcePrincipal = $principal.ForcePrincipal

                $identity = Resolve-Identity -Identity $principalName
                $currentPrincipalAccess = $currentAcl.Audit.Where({$_.IdentityReference -eq $identity.Name})

                foreach ($access in $currentPrincipalAccess)
                {
                    $auditFlags = $access.AuditFlags.ToString()
                    $fileSystemRights = $access.FileSystemRights.ToString().Split(',').Trim()
                    $Inheritance = Get-NtfsInheritenceName -InheritanceFlag $access.InheritanceFlags.value__ -PropagationFlag $access.PropagationFlags.value__

                    $cimFileSystemAuditRule += New-CimInstance -ClientOnly -Namespace $nameSpace -ClassName FileSystemAuditRule -Property @{
                        AuditFlags = $auditFlags
                        FileSystemRights = @($fileSystemRights)
                        Inheritance = $Inheritance
                        Ensure = ""
                    }
                }

                $cimFileSystemAuditRuleList += New-CimInstance -ClientOnly -Namespace $nameSpace -ClassName FileSystemAuditRuleList -Property @{
                    Principal = $principalName
                    ForcePrincipal = $forcePrincipal
                    AuditRuleEntry = [Microsoft.Management.Infrastructure.CimInstance[]]@($cimFileSystemAuditRule)
                }
            }
        }
        else
        {
            $message = $localizedData.AclNotFound -f $inputPath
            Write-Verbose -Message $message
        }
    }
    else
    {
        $Message = $localizedData.ErrorPathNotFound -f $inputPath
        Write-Verbose -Message $Message
    }

    $returnValue = @{
        Force = $Force
        Path = $inputPath
        AuditRuleList = $cimfileSystemAuditRuleList
    }

    return $returnValue
}

<#
    .SYNOPSIS
        Changes the state to desired state.
#>

function Set-TargetResource
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Path,

        [Parameter(Mandatory=$true)]
        [Microsoft.Management.Infrastructure.CimInstance[]]
        $AuditRuleList,

        [Parameter()]
        [bool]
        $Force = $false
    )

    if (Test-Path -Path $Path)
    {
        $currentAcl = Get-AuditAcl -Path $Path

        if ($null -ne $currentAcl)
        {
            if ($Force)
            {
                # If inheritance is set, disable it and clear inherited audit rules
                if (-not $currentAcl.AreAuditRulesProtected)
                {
                    $currentAcl.SetAuditRuleProtection($true, $false)
                    Write-Verbose -Message ($localizedData.ResetDisableInheritance)
                }

                # Removing all audit rules to ensure a blank list
                if ($null -ne $currentAcl.Audit)
                {
                    foreach ($rule in $currentAcl.Audit)
                    {
                        $ruleRemoval = $currentAcl.RemoveAuditRule($rule)

                        if (-not $ruleRemoval)
                        {
                            $currentAcl.RemoveAuditRuleSpecific($rule)
                        }

                        Write-CustomVerboseMessage -Action 'ActionRemoveAudit' -Path $path -Rule $rule
                    }
                }
            }

            foreach ($auditRuleItem in $AuditRuleList)
            {
                $principal = $auditRuleItem.Principal
                $identity = Resolve-Identity -Identity $principal
                $identityRef = New-Object System.Security.Principal.NTAccount($identity.Name)
                $actualAce = $currentAcl.Audit.Where({$_.IdentityReference -eq $identity.Name})
                $aclRules = ConvertTo-FileSystemAuditRule -AuditRuleList $auditRuleItem -IdentityRef $identityRef
                $results = Compare-FileSystemAuditRule -Expected $aclRules -Actual $actualAce
                $expected += $results.Rules
                $toBeRemoved += $results.Absent

                if ($auditRuleItem.ForcePrincipal)
                {
                    $toBeRemoved += $results.ToBeRemoved
                }
            }

            $isInherited = $toBeRemoved.Rule.Where({$_.IsInherited -eq $true}).Count

            if ($isInherited -gt 0)
            {
                $currentAcl.SetAuditRuleProtection($true,$true)
                Set-Acl -Path $path -AclObject $currentAcl
                $currentAcl = $currentAcl = Get-AuditAcl -Path $Path
            }

            foreach ($rule in $expected)
            {
                if ($rule.Match -eq $false)
                {
                    $currentAcl.AddAuditRule($rule.Rule)
                    Write-CustomVerboseMessage -Action 'ActionAddAudit' -Path $path -Rule $rule.Rule
                }
            }

            foreach ($rule in $toBeRemoved.Rule)
            {
                $ruleRemoval = $currentAcl.RemoveAuditRule($rule)
                if (-not $ruleRemoval)
                {
                    $currentAcl.RemoveAuditRuleSpecific($rule)
                }

                Write-CustomVerboseMessage -Action 'ActionRemoveAudit' -Path $path -Rule $rule
            }

            Set-Acl -Path $path -AclObject $currentAcl
        }
        else
        {
            $message = $localizedData.AclNotFound -f $path
            Write-Verbose -Message $message
        }
    }
    else
    {
        $message = $localizedData.ErrorPathNotFound -f $path
        Write-Verbose -Message $message
    }
}

<#
    .SYNOPSIS
        Test the current state of the resource.
#>

function Test-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Path,

        [Parameter(Mandatory=$true)]
        [Microsoft.Management.Infrastructure.CimInstance[]]
        $AuditRuleList,

        [Parameter()]
        [bool]
        $Force = $false
    )

    $aclRules = @()

    $inDesiredState = $true
    $inputPath = Get-InputPath($Path)
    $currentAuditAcl = Get-Acl -Path $inputPath -Audit -ErrorAction Stop

    if ($null -ne $currentAuditAcl)
    {
        if ($Force)
        {
            if ($currentAcl.AreAccessRulesProtected -eq $false)
            {
                Write-Verbose -Message ($localizedData.InheritanceDetectedForce -f $Force, $inputPath)
                return $false
            }

            foreach ($auditRuleItem in $AuditRuleList)
            {
                $principal = $auditRuleItem.Principal
                $identity = Resolve-Identity -Identity $principal
                $identityRef = New-Object System.Security.Principal.NTAccount($identity.Name)
                $aclRules += ConvertTo-FileSystemAuditRule -AuditRuleList $auditRuleItem -IdentityRef $identityRef
            }

            $actualAce = $currentAuditAcl.Audit
            $results = Compare-FileSystemAuditRule -Expected $aclRules -Actual $actualAce -Force $auditRuleItem.ForcePrincipal
            $expected = $results.Rules
            $absentToBeRemoved = $results.Absent
            $toBeRemoved = $results.ToBeRemoved
        }
        else
        {
            foreach ($auditRuleItem in $AuditRuleList)
            {
                $principal = $auditruleItem.Principal
                $identity = Resolve-Identity -Identity $principal
                $identityRef = New-Object System.Security.Principal.NTAccount($identity.Name)
                $aclRules = ConvertTo-FileSystemAuditRule -AuditRuleList $auditRuleItem -IdentityRef $identityRef
                $actualAce = $currentAuditAcl.Audit.Where( {$_.IdentityReference -eq $identity.Name})
                $results = Compare-FileSystemAuditRule -Expected $aclRules -Actual $actualAce -Force $auditRuleItem.ForcePrincipal
                $expected += $results.Rules
                $absentToBeRemoved += $results.Absent

                if ($auditRuleItem.ForcePrincipal)
                {
                    $toBeRemoved += $results.ToBeRemoved
                }
            }
        }

        foreach ($rule in $expected)
        {
            if ($rule.Match -eq $false)
            {
                Write-CustomVerboseMessage -Action 'ActionMissPresentPerm' -Path $inputPath -Rule $rule.rule
                $inDesiredState = $false
            }
        }

        if ($absentToBeRemoved.Count -gt 0)
        {
            foreach ($rule in $absentToBeRemoved.Rule)
            {
                Write-CustomVerboseMessage -Action 'ActionAbsentPermission' -Path $inputPath -Rule $rule
            }

            $inDesiredState = $false
        }

        if ($toBeRemoved.Count -gt 0)
        {
            foreach ($rule in $toBeRemoved.Rule)
            {
                Write-CustomVerboseMessage -Action 'ActionNonMatchPermission' -Path $inputPath -Rule $rule
            }

            $inDesiredState = $false
        }
    }
    else
    {
        Write-Verbose -Message ($localizedData.AclNotFound -f $inputPath)
        $inDesiredState = $false
    }

    return $inDesiredState
}

<#
    .SYNOPSIS
        Converts a CimInstance to a File System.Security.AccessControl.FileSystemAuditRule
    
    .PARAMETER AuditRuleList
        A collection of CIM instances to be converted to an FileSystemAuditRule

    .PARAMETER IndentityRef
        Specifies the prinipal to attach to the FileSystemAuditRule
#>

function ConvertTo-FileSystemAuditRule
{
    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param
    (
        [Parameter(Mandatory = $true)]
        [Microsoft.Management.Infrastructure.CimInstance]
        $AuditRuleList,

        [Parameter(Mandatory = $true)]
        [System.Security.Principal.NTAccount]
        $IdentityRef
    )

    $referenceRule = @()

    foreach ($ace in $AuditRuleList.AuditRuleEntry)
    {
        $inheritance = Get-NtfsInheritenceFlag -Inheritance $ace.Inheritance
        $rule = [PSCustomObject]@{
            Rules = New-Object System.Security.AccessControl.FileSystemAuditRule(
                $IdentityRef,
                $ace.FileSystemRights,
                $inheritance.InheritanceFlag,
                $inheritance.PropagationFlag,
                $ace.AuditFlags
            )

            Ensure = $ace.Ensure
        }

        $referenceRule += $rule
    }

    return $referenceRule
}

<#
    .SYNOPSIS
        Compares desired file system audit rules with the current state.

    .PARAMETER Expected
        Specifies the expected state

    .PARAMETER Actual
        Specifies the current state

    .PARAMETER Force
        Specifies if that the Expected auditRules are the only rules to be applied.
#>

function Compare-FileSystemAuditRule
{
    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param
    (
        [Parameter(Mandatory = $true)]
        [PSCustomObject[]]
        $Expected,

        [Parameter()]
        [System.Security.AccessControl.FileSystemAuditRule[]]
        $Actual,

        [Parameter()]
        [bool]
        $Force = $false
    )

    $results = @()
    $toBeRemoved = @()
    $absentToBeRemoved = @()
    $presentRules = $Expected.Where({$_.Ensure -eq 'Present'}).Rules
    $absentRules = $Expected.Where({$_.Ensure -eq 'Absent'}).Rules

    foreach ($referenceRule in $PresentRules)
    {
        $match = Test-FileSystemAuditRuleMatch -ReferenceRule $referenceRule -DifferenceRule $Actual -Force $Force

        if
        (
            ($match.Count -ge 1) -and
            ($match.FileSystemRights.value__ -ge $referenceRule.FileSystemRights.value__)
        )
        {
            $results += [PSCustomObject]@{
                Rule  = $referenceRule
                Match = $true
            }
        }
        else
        {
            $results += [PSCustomObject]@{
                Rule  = $referenceRule
                Match = $false
            }
        }
    }

    foreach ($referenceRule in $AbsentRules)
    {
        $match = Test-FileSystemAuditRuleMatch -ReferenceRule $referenceRule -DifferenceRule $Actual -Force $Force

        if ($match.Count -gt 0)
        {
            $absentToBeRemoved += [PSCustomObject]@{
                Rule = $match
            }
        }
    }

    foreach ($referenceRule in $Actual)
    {
        $match = Test-FileSystemAuditRuleMatch -ReferenceRule $referenceRule -DifferenceRule $Expected.Rules -Force $Force

        if ($match.Count -eq 0)
        {
            $toBeRemoved += [PSCustomObject]@{
                Rule = $referenceRule
            }
        }
    }

    return [PSCustomObject]@{
        Rules = $results
        ToBeRemoved = $toBeRemoved
        Absent = $absentToBeRemoved
    }
}

<#
    .SYNOPSIS
        Tests if files system audit rules match.

    .PARAMETER DifferenceRule
        Specifies the rules in the configuration.

    .PARAMETER ReferenceRule
        Specifies the rules currently applied

    .PARAMETER Force
        Specifies if that the Expected ReferenceRule are the only rules to be applied.
#>

function Test-FileSystemAuditRuleMatch
{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.Security.AccessControl.FileSystemAuditRule[]]
        [AllowEmptyCollection()]
        $DifferenceRule,

        [Parameter(Mandatory = $true)]
        [System.Security.AccessControl.FileSystemAuditRule]
        $ReferenceRule,

        [Parameter(Mandatory = $true)]
        [bool]
        $Force
    )

    if ($Force)
    {
        $DifferenceRule.Where({
            $_.FileSystemRights -eq $ReferenceRule.FileSystemRights -and
            $_.AuditFlags -eq $ReferenceRule.AuditFlags -and
            $_.InheritanceFlags -eq $ReferenceRule.InheritanceFlags -and
            $_.PropagationFlags -eq $ReferenceRule.PropagationFlags -and
            $_.IdentityReference -eq $ReferenceRule.IdentityReference
        })
    }
    else
    {
        $DifferenceRule.Where({
            ($_.FileSystemRights.value__ -band $ReferenceRule.FileSystemRights.value__) -match
            "$($_.FileSystemRights.value__)|$($ReferenceRule.FileSystemRights.value__)" -and
            (($_.AuditFlags.value__ -eq 3 -and $ReferenceRule.AuditFlags.value__ -in 1..3) -or
            ($_.AuditFlags.value__ -in 1..3 -and $ReferenceRule.AuditFlags.value__ -eq 0) -or
            ($_.AuditFlags.value__ -eq $ReferenceRule.AuditFlags.value__)) -and

            (($_.InheritanceFlags.value__ -eq 3 -and $ReferenceRule.InheritanceFlags.value__ -in 1..3) -or
            ($_.InheritanceFlags.value__ -in 1..3 -and $ReferenceRule.InheritanceFlags.value__ -eq 0) -or
            ($_.InheritanceFlags.value__ -eq $ReferenceRule.InheritanceFlags.value__)) -and
            (($_.PropagationFlags.value__ -eq 3 -and $ReferenceRule.PropagationFlags.value__ -in 1..3) -or
            ($_.PropagationFlags.value__ -in 1..3 -and $ReferenceRule.PropagationFlags.value__ -eq 0) -or
            ($_.PropagationFlags.value__ -eq $ReferenceRule.PropagationFlags.value__)) -and

            $_.IdentityReference -eq $ReferenceRule.IdentityReference
        })
    }
}

<#
    .SYNOPSIS
        Retrieves the Audit authorization data from a filesystem object

    .PARAMETER PATH
        Specifies the path to the target folder.
#>
#>
function Get-AuditAcl
{
    [CmdletBinding()]
    [OutputType([System.Security.AccessControl.DirectorySecurity])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Path
    )

    $sacl =  (Get-Item -Path $Path).GetAccessControl('All')
    $auditRules = $sacl.GetAuditRules($true,$true,[System.Security.Principal.NTAccount])
    $sacl | Add-Member -MemberType NoteProperty -Value $auditRules -Name Audit

    return $sacl
}

Export-ModuleMember -Function @('Get-TargetResource','Set-TargetResource','Test-TargetResource')