# -*-mode:powershell; fill-column:80;-*-
#
# fix_broken_sids.ps1 - Find and heal broken SIDs
#
################################################
# $Author: Andreas Spindler <info@visualco.de>$
# $Compile: powershell -executionpolicy bypass -noexit -file .\\fix_broken_sids.ps1          H:\\bak\\rb$
# $Compile: powershell -executionpolicy bypass -noexit -file .\\fix_broken_sids.ps1 -verbose H:\\bak\\rb$
# $Maintained at: http://www.visualco.de$
# $ Writestamp: 2015-01-04 17:46:55$

param (
    [string]$dir       = "",
    [string]$localuser = "",
    [switch]$heal      = $false,
    [switch]$scan      = $false,
    [switch]$verbose   = $false,
    [switch]$quiet     = $false,
    [switch]$norecurse = $false,
    [switch]$stop      = $false,
    [switch]$help      = $false
)

$global:exitcode = 0

function Pause() {
    Read-Host "Please press any key to continue"
}
function MaybePause() {
    if ($verbose) { Pause }
}
function Error([string]$s) {
    $Host.UI.WriteErrorLine("ERROR: " + $s)
    $global:exitcode = -1
}
function Verbose([string]$s) {  # if $verbose
    if ($verbose) { Write-Host $s; }
}

function Chat([string]$s) {     # unless $quiet
    if (!$quiet) { Write-Host $s; }
}

if ($dir -eq "" -or $help) {
    Write-Host @"
USAGE

    fix_broken_sids [ -dir DIRECTORY [ -norecurse ] ]
            [ -scan | { -heal [-localuser NAME] } | -reset ]
            
Use "-heal"  or "-reset" to fix  ACL and owner  SID issues. When called  with no
options this is  equivalent to a dry run  that will only scan for  issues in the
current directory and sub-directories.

VENUE

Access Control List (ACL) and Owner-SID  Repair Utility. Replaces broken SIDs in
ACLs with  the SID  of a  known user on  this machine.  Broken SIDs  are unknown
domain  permissions on  files or  folders. Broken  SIDs refer  to an  account on
another domain,  or local account  on another  maschine. Files and  folders that
inherit permissions  are (naturally)  ignored, because inherting  permissions is
the default  on Windows, so  that many files and  folders do not  actually carry
limiting ACLs.

OPTIONS

    -dir DIRECTORY:
        Find and replace broken SIDs in the specified directory.

    -norecurse:
         Do not descend into sub-directories.

    -scan:
         Find broken SIDs.

    -heal:
         Replace broken SIDs with the local user, and give this user full
         access. Attempt to take ownership if necessary.

    -localuser NAME:
         The local user that contributes the good SID. Defaults to the current
         user. Useful only in conjunction with -heal.

    -reset:
        Take ownership and reset ACLs of files in directory to the Windows
        default behavior of inherting ACLs. Carried out to the ICACLS utility.
        Sweeps all specific ACLs away, unlike -heal, which only sweeps unknown
        entries.

    -stop:
         Stop after first directory with broken SIDs.

    -quiet:
         Enable quiet mode.

    -verbose:
         Enable verbose mode.

DESCRIPTION

Symptoms:

When we copy files or folders from one server (or workstation) to another server
(or workstation)  that is  a member  of a different  domain, the  access control
entries for  the first server's local  groups appear as "Unknown"  on the second
server. ACL SIDs from other computers are not  a problem as long as the ACL also
has sufficient  entries for the  other domain or users.  But a broken  owner SID
prevent users  from accessing their  own files, and  can even prevent  using the
"Security" tab in the property dialog of  a directory, or the ICACLS tool on the
command-line.

Cause:

The security  identifier (SID) values for  the local groups on  the first server
are valid only  on that server. (The  SID values for other user  accounts on the
first server are also valid only on that server.) These SID values are not valid
on a server  that is located on  a different domain. The second  server does not
recognize the SID values for the first server's local groups and user accounts.

Privileges:

This  tool is  designed  for use  by  administrators. It  tries  to raise  these
privileges temporarily. Still  some actions may fail or  generate error messages
if the user does not have the following privileges:

    SeBackupPrivilege (Back Up Files and Directories)
    SeChangeNotifyPrivilege (Bypass Traverse Checking)
    SeRestorePrivilege (Restore Files and Directories)
    SeSecurityPrivilege (Manage Auditing and Security Log)
    SeTakeOwnershipPrivilege (Take Ownership of Files or Other Objects)
    SeTcbPrivilege (Act As Part of the Operating System)

To verify the settings for your execution policy at the PowerShell prompt:

    > powershell
    Windows PowerShell
    Copyright (C) 2009, 2013 Microsoft Corporation. Alle Rechte vorbehalten.
    PS H:\> get-executionpolicy
    Restricted

To enable execution of PowerShell-scripts, as an Administrator run:

    PS H:\> Set-ExecutionPolicy RemoteSigned

Alternatively,  you can  set the  execution  policy to  AllSigned (all  scripts,
including those  you write yourself, must  be signed by a  trusted publisher) or
Unrestricted  (all scripts  will run,  regardless of  where they  come from  and
whether or not they've been signed). More information is provided by:

    > Get-Help About_Signing

On an End user machine call:

    powershell -noprofile -executionpolicy bypass -file .\script.ps1

RESULT

Exit code is 0 if  no ACLs need to be updated, or -1 if there  was an error or 1
if ACLs have been updated without error.

"@
    MaybePause
    exit
}

if (! (Test-Path -path $dir)) {
    Error "${dir}: path not found"; exit -1
}

if (! ($heal -or $scan)) { $scan = $true }
if ($verbose) { $quiet = $false }

$FSO = New-Object -com scripting.filesystemobject

$localmachine = [Environment]::MachineName
if ($localuser) {
    $username      = "${localmachine}\${localuser}"
    #$userwinid     = ([System.Security.Principal.WindowsIdentity] $username)
    $useriscurrent = $false
} else {
    $userwinid     = [System.Security.Principal.WindowsIdentity]::GetCurrent()
    $username      = $userwinid.Name
    $useriscurrent = $true
}

if (!$username) {
    Write-Warning "missing user name"; exit -1;
}
$useraccessrule  = New-Object System.Security.AccessControl.FileSystemAccessRule(
    $username, "FullControl", "ContainerInherit, ObjectInherit", "None", "Allow")
$useraccount     = New-Object System.Security.Principal.NTAccount($username)
$useraccountname = $useraccount.Value
$usersid         = $useraccount.Translate([System.Security.Principal.SecurityIdentifier])

if (!$usersid) { Write-Warning "could not retrieve SID of user '$username'"; exit -1; }

$haveadminprivs  = $false
if (([Security.Principal.WindowsPrincipal]
     [Security.Principal.WindowsIdentity]::
     GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
    # Note that this script runs perfectly with Administrator rights. However,
    # we want to prevent setting the Administrator as owner or in ACLs without
    # an explicit statement.
    $s = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
    $haveadminprivs = $true
    Write-Warning "'$s' has Administrator privileges (the current user)"
    if ($heal) {
        if ($useriscurrent) {
            Write-Warning "as an Administrator please explicitly specify '-localuser NAME'"
            exit -1
        }
    }
}

$userhasadminprivs = $false
if ($userwinid) {
    if (([Security.Principal.WindowsPrincipal] $userwinid).
        IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
        $userhasadminprivs = $true
    }
}

$global:pathcount = 0
$global:goodpathcount = 0
$global:brokenACESIDs = 0
$global:brokenOwnerSIDs = 0
$global:updatedACECount = 0
$global:updatedOwnerCount = 0
$global:failedupdatedACECount = 0
$global:failedupdatedOwnerCount = 0

################################################################################
# UTILITIES
################################################################################

########################################################################
# functions Force-Owner, Force-ACL
#
# The folling code add the AddPrivilege and RemovePrivilege functions, which are
# useful to active a privilege in the user's token on the fly. These functions
# are required to solve a common problem with the Set-Acl cmdlet.
#
# Normally, in Windows NT-family security, you cannot set the owner of a secured
# object (such as a file or folder, or a registry key) to someone else – you can
# only take ownership for yourself or a group you’re a member of. If you’re
# unprivileged you can only take ownership if the Discretionary Access Control
# List (ACL) gives you permission to take ownership. There is also a privilege –
# SE_TAKE_OWNERSHIP_NAME – which allows you to take ownership even if you’re not
# permitted in the ACL. This privilege is normally and by default only assigned
# to the Administrators group.
#
# This ability to take ownership is in marked contrast to Unix where administrators can set
# ownership, but users cannot take it.
#
# Under Windows NT privileges come in two types - those which are enabled by
# default if they are permitted to a user (or a group of which the user is a
# member), and those which are disabled by default. Of the latter type, some
# Win32 APIs enable the privileges they require automatically. the
# SE_RESTORE_NAME is not one; you must call the Win32 API AdjustTokenPrivileges
# to enable the privilege if not already enabled. SE_RESTORE_NAME was meant for
# Backup Administrators. If you have the SE_RESTORE_NAME privilege enabled
# you’re permitted to set the owner of a secured object. Which makes sense,
# really, as you want restored data to have the same owner as it did when it was
# backed up.
#
# By default the Administrators and Backup Operators groups only have the
# SE_BACKUP_NAME and SE_RESTORE_NAME privileges. You can modify this and other
# privileges through Group Policy – for your own machine use the Local Security
# Policy shortcut in Administrative Tools. It’s really not recommended to modify
# these settings unless you’re absolutely certain you know what you’re doing. If
# you must, see under Security Settings > Local Policies > User Rights
# Assignment.
#
# http://cosmoskey.blogspot.de/2010/07/setting-owner-on-acl-in-powershell.html
# http://www.powershellpraxis.de/index.php/ntfs-filesystem/datei-und-verzeichnisberechtigungen

$cosmoscode = @"
using System;
using System.Runtime.InteropServices;
namespace CosmosKey.Utils
{
    public class TokenManipulator
    {
        [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)]
        internal static extern bool AdjustTokenPrivileges(IntPtr htok, bool disall, ref TokPriv1Luid newst, int len, IntPtr prev, IntPtr relen);
        [DllImport("kernel32.dll", ExactSpelling = true)]
        internal static extern IntPtr GetCurrentProcess();
        [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)]
        internal static extern bool OpenProcessToken(IntPtr h, int acc, ref IntPtr  phtok);
        [DllImport("advapi32.dll", SetLastError = true)]
        internal static extern bool LookupPrivilegeValue(string host, string name, ref long pluid);

        [StructLayout(LayoutKind.Sequential, Pack = 1)]
        internal struct TokPriv1Luid {
            public int Count;
            public long Luid;
            public int Attr;
        }

        internal const int SE_PRIVILEGE_DISABLED = 0x00000000;
        internal const int SE_PRIVILEGE_ENABLED = 0x00000002;
        internal const int TOKEN_QUERY = 0x00000008;
        internal const int TOKEN_ADJUST_PRIVILEGES = 0x00000020;

        public const string SE_ASSIGNPRIMARYTOKEN_NAME = "SeAssignPrimaryTokenPrivilege";
        public const string SE_AUDIT_NAME = "SeAuditPrivilege";
        public const string SE_BACKUP_NAME = "SeBackupPrivilege";
        public const string SE_CHANGE_NOTIFY_NAME = "SeChangeNotifyPrivilege";
        public const string SE_CREATE_GLOBAL_NAME = "SeCreateGlobalPrivilege";
        public const string SE_CREATE_PAGEFILE_NAME = "SeCreatePagefilePrivilege";
        public const string SE_CREATE_PERMANENT_NAME = "SeCreatePermanentPrivilege";
        public const string SE_CREATE_SYMBOLIC_LINK_NAME = "SeCreateSymbolicLinkPrivilege";
        public const string SE_CREATE_TOKEN_NAME = "SeCreateTokenPrivilege";
        public const string SE_DEBUG_NAME = "SeDebugPrivilege";
        public const string SE_ENABLE_DELEGATION_NAME = "SeEnableDelegationPrivilege";
        public const string SE_IMPERSONATE_NAME = "SeImpersonatePrivilege";
        public const string SE_INC_BASE_PRIORITY_NAME = "SeIncreaseBasePriorityPrivilege";
        public const string SE_INCREASE_QUOTA_NAME = "SeIncreaseQuotaPrivilege";
        public const string SE_INC_WORKING_SET_NAME = "SeIncreaseWorkingSetPrivilege";
        public const string SE_LOAD_DRIVER_NAME = "SeLoadDriverPrivilege";
        public const string SE_LOCK_MEMORY_NAME = "SeLockMemoryPrivilege";
        public const string SE_MACHINE_ACCOUNT_NAME = "SeMachineAccountPrivilege";
        public const string SE_MANAGE_VOLUME_NAME = "SeManageVolumePrivilege";
        public const string SE_PROF_SINGLE_PROCESS_NAME = "SeProfileSingleProcessPrivilege";
        public const string SE_RELABEL_NAME = "SeRelabelPrivilege";
        public const string SE_REMOTE_SHUTDOWN_NAME = "SeRemoteShutdownPrivilege";
        public const string SE_RESTORE_NAME = "SeRestorePrivilege";
        public const string SE_SECURITY_NAME = "SeSecurityPrivilege";
        public const string SE_SHUTDOWN_NAME = "SeShutdownPrivilege";
        public const string SE_SYNC_AGENT_NAME = "SeSyncAgentPrivilege";
        public const string SE_SYSTEM_ENVIRONMENT_NAME = "SeSystemEnvironmentPrivilege";
        public const string SE_SYSTEM_PROFILE_NAME = "SeSystemProfilePrivilege";
        public const string SE_SYSTEMTIME_NAME = "SeSystemtimePrivilege";
        public const string SE_TAKE_OWNERSHIP_NAME = "SeTakeOwnershipPrivilege";
        public const string SE_TCB_NAME = "SeTcbPrivilege";
        public const string SE_TIME_ZONE_NAME = "SeTimeZonePrivilege";
        public const string SE_TRUSTED_CREDMAN_ACCESS_NAME = "SeTrustedCredManAccessPrivilege";
        public const string SE_UNDOCK_NAME = "SeUndockPrivilege";
        public const string SE_UNSOLICITED_INPUT_NAME = "SeUnsolicitedInputPrivilege";

        public static bool AddPrivilege(string privilege) {
            try {
                bool retVal;
                TokPriv1Luid tp;
                IntPtr hproc = GetCurrentProcess();
                IntPtr htok = IntPtr.Zero;
                retVal = OpenProcessToken(hproc, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, ref htok);
                tp.Count = 1;
                tp.Luid = 0;
                tp.Attr = SE_PRIVILEGE_ENABLED;
                retVal = LookupPrivilegeValue(null, privilege, ref tp.Luid);
                retVal = AdjustTokenPrivileges(htok, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero);
                return retVal;
            } catch (Exception ex) {
                throw ex;
            }
        }

        public static bool RemovePrivilege(string privilege) {
            try {
                bool retVal;
                TokPriv1Luid tp;
                IntPtr hproc = GetCurrentProcess();
                IntPtr htok = IntPtr.Zero;
                retVal = OpenProcessToken(hproc, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, ref htok);
                tp.Count = 1;
                tp.Luid = 0;
                tp.Attr = SE_PRIVILEGE_DISABLED;
                retVal = LookupPrivilegeValue(null, privilege, ref tp.Luid);
                retVal = AdjustTokenPrivileges(htok, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero);
                return retVal;
            } catch (Exception ex) {
                throw ex;
            }
        }
    }
}
"@

$global:hackaclfailed = $false

function Forcel-ACL {
    param(
        [System.Security.AccessControl.DirectorySecurity]$aclobject = $(throw "Mandatory parameter -aclobject missing."),
        $path = $(throw "Mandatory parameter -path missing.")
    )
    if (-not (Test-Path $path)) { throw "Path '$path' not found." }
    if ($aclobject -eq $null)   { throw "The ACL is NULL." }
    $errpref = $ErrorActionPreference
    $ErrorActionPreference= "silentlycontinue"
    Add-Type $cosmoscode
    $type = [CosmosKey.Utils.TokenManipulator]
    $ErrorActionPreference = $errpref
    if ($type -eq $null) { Add-Type $cosmoscode }
    [void][CosmosKey.Utils.TokenManipulator]::AddPrivilege([CosmosKey.Utils.TokenManipulator]::SE_RESTORE_NAME)
    [void][CosmosKey.Utils.TokenManipulator]::AddPrivilege([CosmosKey.Utils.TokenManipulator]::SE_TAKE_OWNERSHIP_NAME)
    # & whoami /priv
    # Pause
    $global:hackaclfailed = $false
    $Error.clear()
    Set-Acl -Path $path -AclObject $aclobject -passthru 2>&1 >$null
    if (-not $?) { Error $Error; $global:hackaclfailed = $true }
    [void][CosmosKey.Utils.TokenManipulator]::RemovePrivilege([CosmosKey.Utils.TokenManipulator]::SE_TAKE_OWNERSHIP_NAME)
    [void][CosmosKey.Utils.TokenManipulator]::RemovePrivilege([CosmosKey.Utils.TokenManipulator]::SE_RESTORE_NAME)
}

function Force-Owner {
    param(
        [System.Security.Principal.IdentityReference]$principal = $(throw "Mandatory parameter -principal missing."),
        $path = $(throw "Mandatory parameter -path missing.")
    )
    if (-not (Test-Path $path)) { throw "Path '$path' not found." }
    if ($principal -eq $null)   { throw "Principal is NULL." }
    $errpref = $ErrorActionPreference
    $ErrorActionPreference= "silentlycontinue"
    Add-Type $cosmoscode
    $type = [CosmosKey.Utils.TokenManipulator]
    $ErrorActionPreference = $errpref
    if ($type -eq $null) { Add-Type $cosmoscode }
    $acl = Get-Acl $path
    $acl.psbase.SetOwner($principal)
    [void][CosmosKey.Utils.TokenManipulator]::AddPrivilege([CosmosKey.Utils.TokenManipulator]::SE_RESTORE_NAME)
    [void][CosmosKey.Utils.TokenManipulator]::AddPrivilege([CosmosKey.Utils.TokenManipulator]::SE_TAKE_OWNERSHIP_NAME)
    $global:hackaclfailed = $false
    $Error.clear()
    Set-Acl -Path $path -AclObject $acl -passthru 2>&1 >$null
    if (-not $?) { Error $Error; $global:hackaclfailed = $true }
    [void][CosmosKey.Utils.TokenManipulator]::RemovePrivilege([CosmosKey.Utils.TokenManipulator]::SE_TAKE_OWNERSHIP_NAME)
    [void][CosmosKey.Utils.TokenManipulator]::RemovePrivilege([CosmosKey.Utils.TokenManipulator]::SE_RESTORE_NAME)
}

########################################################################
# function Heal-Broken-SIDs
#
# Stops if
#    - stopping after first broken SID (scan or heal)
#    - healing failed
#
$global:isbrokenowner = $false
$global:brokenaces = @()

function Is-Broken()
{
    if ($global:isbrokenowner -or $global:brokenaces.Length -or $global:exitcode) { return $true } else { return $false }
}

function Heal-Broken-SIDs([string]$path)
{
    $global:pathcount++
    $global:brokenaces = @()
    $global:isbrokenowner = $false
    $tab = '    '
    try {
        Verbose "${path}:"
        $folder = $FSO.GetFolder($path)
        if (!$folder) { Write-Warning "unable to access folder"; throw }

        ################################################################
        #   I. Get the folder's ACL and retrieve its onwer.
        #
        # Determine which Account the owner (SID) of the folder belongs to on
        # this domain. When the ACL cannot be retrieved probably the directory
        # name contains square brackets. Directories with square brackets cause
        # problems in PowerShell This is especially bad because it is likely, in
        # this case, that the path had been returned by $folder.SubFolders.
        #
        # http://stackoverflow.com/questions/9416680/getaccesscontrol-on-a-directory-using-powershell-returns-no-data
        #
        $A = Get-Acl $path
        if (!$A) {
            $di = Get-Item -literalpath $path
            $A = $di.GetAccessControl()
            if (!$A) {                                                  # this is something else
                Write-Warning "${path}: unable to read ACL of folder"
                Chat "${tab}HINT: $_"; throw
            }
        }
        Verbose "${tab}ok, retrieved folder's ACL"

        $folderownersid = $null
        try {
            $o = $A.Owner
            $b = New-Object System.Security.Principal.NTAccount($o)
            Verbose "${tab}ok, retrieved folder's owner: ${o}"
            $folderownersid = $b.Translate([System.Security.Principal.SecurityIdentifier])
        } catch {
            $global:brokenOwnerSIDs++
            $global:isbrokenowner = $true
        }
        if ($folderownersid) {
            Verbose "${tab}ok, retrieved folder's owner SID: ${folderownersid}"
        } else {
            $o = $A.Owner
            Write-Warning "${path}: broken owner: unknown SID '${o}'"
        }

        ################################################################
        #  II. Enumerate broken ACE's
        #
        # ACE's are stored in the Access property of an ACL object. Filter out
        # inherited ACE's. SIDs of unknown accounts CANNOT be translated into an
        # NTAccount object.
        #
        if (1) {
            $A.Access | ? {-not $_.IsInherited} | % {
                $accessrule = $_
                try {
                    $ntaccount = $accessrule.IdentityReference.Translate([System.Security.Principal.NTAccount])
                } catch {
                    $x = $accessrule.IdentityReference.Value
                    if ($x) {
                        $global:brokenaces += $x
                        $global:brokenACESIDs++
                    } else {                     # value is null, assume the owner is an invalid SID
                        Write-Warning "could not retrieve ACE"
                    }
                }
            }
            $l = $global:brokenaces.Length                              # print broken ACLs
            if ($l) {
                for ($i = 1; $i -le $l; $i++) {
                    $s = $global:brokenaces[$i - 1]
                    Write-Warning "${path}: broken ACE #${i}/${l}: unknown SID '$s'"
                }
            }
            if ($optverbose) {
                Get-Acl $path | Select -Expand Access
            }
        }

        ################################################################
        # III. Heal broken owner
        #
        # Set new onwer to $useraccount, which is the -localuser NAME, or the
        # current user.
        #
        # Generally only ACLs are inherited; ownership is not inherited under
        # NTFS. Ownership it has to be set explicitly. Since only the owner can
        # change the ACL (or Administrators with SeTakeOwnership) we have to
        # heal broken owners before broken ACLs. If the current user is an
        # Administrator the account require the SeTakeOwnership to set a new
        # owner.
        #
        # http://www.heise.de/security/artikel/Vistas-Integrity-Level-Teil-1-270896.html
        #
        # The TAKEOWN utility can assigns ownership only to the current user.
        # ICALCS can specify an extra user (/T enables recursion).
        #
        # http://mikedimmick.blogspot.de/2005/02/how-to-set-owner-of-object-in-windows.html
        #
        if ($heal) {
            if ($global:isbrokenowner) {
                $s = $A.Owner
                Verbose "${tab}setting owner to '$useraccountname'"
                $A.SetOwner($useraccount)
                try {
                    Set-Acl -path $path -aclobject $A  2>&1 >$null
                } catch {
                }

                if (($A = Get-Acl $path).Owner -ne $useraccountname) {
                    Verbose "${tab}${tab}setting owner failed (Set-Acl)"
                    Verbose "${tab}${tab}running ICACLS '$path' /C /SETOWNER '$username'"
                    & ICACLS $path /C /SETOWNER "$username" # 2>&1 >$null

                    if (($A = Get-Acl $path).Owner -ne $useraccountname) {
                        Verbose "${tab}${tab}${tab}setting owner failed again (ICACLS)"
                        Force-Owner -principal $useraccount -path $path
                        if ($global:hackaclfailed) {
                            # Most likely a permission problem. When the ACL has
                            # no ACEs no user is granted access, or there are
                            # ACEs, but insufficient rights. Simply try to grant
                            # everyone full rights, and Forcel-ACL again.
                            Error "unable to update ACL with raised privileges: permission problem?"
                            Verbose "${tab}trying to get access (ICACLS)..."
                            & ICACLS $path /C /GRANT 'Everyone:(OI)(CI)F'
                            Force-Owner -principal $useraccount -path $path
                            if ($global:hackaclfailed) { 
                                Verbose "${tab}sorry, no luck"
                            }
                        }

                        if (($A = Get-Acl $path).Owner -ne $useraccountname) {
                            $global:failedupdatedOwnerCount++
                            Error "could not set new owner '$useraccount'"
                            throw
                        }
                    }
                }

                $s = $A.Owner
                Verbose "${tab}ok, the new owner now is '$s'"
                $global:updatedOwnerCount++
            } else {
                Verbose "${tab}ok, the present owner is '$useraccountname' (nothing to change)"
            }
        }

        ################################################################
        # III. Heal broken ACLs
        #
        # http://andyarismendi.blogspot.de/2012/02/fixing-unresolvable-ntfs-acl-accounts.html
        #
        if ($heal) {
            if ($global:brokenaces.Length) {
                for ($i = 0; $i -lt $l; $i++) {
                    # Replace broken SID(s) with the SID of the local user, and
                    # grant full access. SetAccessRule adds the specified access
                    # control list (ACL) rule or overwrites any identical ACL
                    # rules that match the FileSystemRights value of the rule
                    # parameter.
                    $s = $global:brokenaces[$i]
                    $fixedsddl = $A.sddl.Replace($s, $usersid)
                    $A.SetSecurityDescriptorSddlForm($fixedsddl)
                    $A.SetAccessRule($useraccessrule)

                    # Set-Acl funktioniert nur wenn der der neue Besitzer gleich
                    # dem angemeldeten Benutzer ist. Sonst erhält man die
                    # Fehlermeldung:
                    #       Set-Acl : Die Sicherheits-ID darf nicht der Besitzer dieses Objekts sein.
                    Set-Acl -path $path -aclobject $A 2>&1 >$null
                    if (-not $?) {
                        Error "could not update ACL (with Set-ACL)"
                        Verbose "${tab}trying to update ACL with raised privileges..."
                        Forcel-ACL -path $path -aclobject $A

                        if ($global:hackaclfailed) {
                            Error "unable to update ACL with raised privileges: permission problem?"
                            Verbose "${tab}trying to get access (running ICACLS)..."
                            & ICACLS $path /C /GRANT 'Everyone:(OI)(CI)F'
                            Verbose "${tab}trying to update ACL again..."
                            Forcel-ACL -path $path -aclobject $A
                        }

                        if (-not $global:hackaclfailed) {
                            # Test the ACL of $path again for ACEs with unknown
                            # SIDs. ACEs are returned by the Access method of an
                            # ACL object. Translate this into some NTAccount
                            # object (in the local domain). Translate throws if
                            # this fails (then the SID is unknown). Naturally we
                            # aren't interested in ACEs that are inherited. Then
                            # Set-ACL and Forcel-ACL both failed! # Translate
                            # throws when the ACE is unknown.
                            $A = Get-Acl $path
                            $A.Access | ? {-not $_.IsInherited} | % {
                                $accessrule = $_
                                try {
                                    $ntaccount = $accessrule.IdentityReference.Translate([System.Security.Principal.NTAccount])
                                    $global:updatedACECount++
                                } catch {
                                    Error "unable to translated updated ACL into an NTAccount of the local domain"
                                    break
                                }
                            } continue # ok, Forcel-ACL worked
                        } else {
                            Error "unable to update ACL: giving up"
                            # probably the volume is corrupt: the user should
                            # CHKDSK /F
                        }
                    } else {
                        continue # ok, Set-ACL worked
                    }

                    ####################################################
                    # Continue with next path or give up.
                    #
                    if (0) {
                        Chat @"

HINTS
-----

Most likely the problem is MISSING PRIVILEGES. Privileges define in detail what
a user can do in the system, and NTFS. Do not expect the Administrator can do
anything. Under Windows the Administrator is not some "super-user" like under
UNIX. When you change the owner of a secured object (e.g. file, directory,
registry key) the user that runs fix_broken_sids, if an Administrator or not,
needs to have the privileges 'SeTakeOwnershipPrivilege' and
'SeRestorePrivilege'. To gain missing privileges run SECPOL.MSC, but this
requires at least Windows Professional.

"@
                        $s = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
                        Chat @"
These are the privileges of ${s}:
"@
                        MaybePause
                        & whoami /priv                                  # list privileges
                        if (!$haveadminprivs) {
                            Verbose @"

You may als run this program as Administrator (currently this account has no
Administrator privileges). There is no super-user under Windows, and setting a
secured objects' owner and access requires some effort:
    (1) only the owner can change ACLs
    (2) Administrators cannot change ACLs unless they're also owners
    (3) Administrators need additional privileges to set a new owner
    (4) generally ownership is not inherited under NTFS; it has to be set explicitly

"@
                        } else {
                            Chat @"
You are '$useraccountname' (an Administrator).

"@
                        }
                    }

                    $global:failedupdatedACECount++
                    $global:exitcode = -1
                    return
                }
            }
        }
        if(!$global:brokenaces.Length) {
            Verbose "${tab}ok, no broken ACEs"
        }

        ################################################################
        #  IV. Recurse subfolders
        #
        $recursenow = !$norecurse
        if (Is-Broken) {
            if ($stop) {
                Verbose "${tab}${tab}...stopping at first broken SID(s)"
                $recursenow = $false
            }
        } else {
            $global:goodpathcount++
        }

        if ($recursenow) {
            try {
                foreach ($i in $folder.subfolders) {
                    Heal-Broken-SIDs -path $i.Path
                    if (Is-Broken) { if ($stop) { break } }
                }
            } catch {           # thrown by $folder.SubFolders
                if ($verbose) {
                    Write-Warning "${path}: cannot descend directory"
                    Chat "${tab}$_"
                }
            }
        }
    } catch {
        Error "exception while trying to heal broken SIDs, stop!"
        if (0) {
            Chat @"

HINTS
-----

Probably the directory entry is inaccessible, and NTFS suffered a corruption.
Try running CHKDSK. Oftenly CHKDSK /F can help when the ACL is defect in a way
that confuses Windows. When CHKDSK has repared the volumn try running
fix_broken_sids (this program) again.

"@
        }
        $global:exitcode = -1
    }
}

################################################################################
# MAIN CODE
################################################################################
#
# Reset ACL to default (inherited ACLs) recursively (-reset)
#
# Find and/or broken owner and ACL SIDs (-heal)
#

Chat @"

VENUE
-----

"@

if ($heal) {
    Chat "Healing unknown SIDs (owners and ACEs) in '$dir'."
    if (!$norecurse) { Chat "Recursion enabled." }
    if ( $stop) { Chat "Stopping after first occurence of a broken SID." }
    if ($useriscurrent) {
        Chat "Broken SIDs will be replaced by the SID of user '$username'."
    } else {
        Chat "Broken SIDs will be replaced by the SID of user '$username', which has been specified as argument."
    }
    Chat "The current user is '$username'."
    if ($userhasadminprivs) {
        Write-Warning "the current user has Administrator privileges"
    } else {
        Chat "The current user has no Administrator privileges."
    }
    Verbose @"
You must be able to modify ACLs. Therefore you must be the owner of the
directory to be healed, or have the user rights 'SeTakeOnwershipPrivilege' and
'SeRestorePrivilege' actived in your token. However, fix_broken_sids raises your
privileges if required.

"@
    MaybePause
    Chat "Running..."
    Heal-Broken-SIDs -path $dir
}

if (-not (Is-Broken)) {
    if ($scan) {
        Chat "Finding broken owners and ACEs in '$dir'."
        if (!$norecurse) { Chat "Recursion enabled." }
        if ( $stop) { Chat "Stopping after first occurence of a broken SID." }
        Chat "Running..."
        Heal-Broken-SIDs -path $dir
    }
}

########################################################################
# Done.
#
if ($heal -or $scan) {
    $bad = $global:pathcount - $global:goodpathcount
    $a = "{0,8}" -f ($global:pathcount)
    $b = "{0,8}" -f ($global:brokenOwnerSIDs + $global:brokenACESIDs)
    $c = "{0,8}" -f ($global:updatedOwnerCount + $global:updatedACECount)
    Chat @"

${a} paths       ( $global:goodpathcount good + $bad bad )
${b} broken SIDs ( $global:brokenOwnerSIDs owner + $global:brokenACESIDs ACE )
${c} healed SIDs ( $global:updatedOwnerCount owner + $global:updatedACECount ACE )

"@
}

if ($global:exitcode) {
    Chat "FAILED ($global:exitcode)"
} else {
    Chat "OK ($global:exitcode)"
}
exit $global:exitcode

# END OF FILE