Export-PermissionPortable.ps1
<#PSScriptInfo
.VERSION 0.0.90 .GUID c7308309-badf-44ea-8717-28e5f5beffd5 .AUTHOR Jeremy La Camera .COMPANYNAME Jeremy La Camera .COPYRIGHT (c) Jeremy La Camera. All rights reserved. .TAGS .LICENSEURI https://github.com/IMJLA/ExportPermission/blob/main/LICENSE .PROJECTURI https://github.com/IMJLA/ExportPermission .ICONURI .EXTERNALMODULEDEPENDENCIES Adsi,SimplePrtg,PsNtfs,PsLogMessage,PsRunspace,PsDfs,PsBootstrapCss,Permission .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES .PRIVATEDATA #> <# .SYNOPSIS Portable version of Export-Permission with all ScriptModule dependencies rolled up into this single .ps1 file Create CSV, HTML, and XML reports of permissions .DESCRIPTION Gets all permissions for the target folder Gets non-inherited permissions for subfolders (if specified) Exports the permissions to CSV Uses ADSI to get information about the accounts and groups listed in the permissions Exports information about the accounts and groups to CSV Uses ADSI to recursively retrieve the members of nested groups Creates an HTML report showing the resultant access of individual accounts Exports information about all accounts with NTFS access to CSV Creates an HTML report of all accounts with NTFS access Outputs an XML-formatted list of common misconfigurations for use win Paessler PRTG Network Monitor as a custom XML sensor .INPUTS None. Pipeline input is not accepted. .OUTPUTS [System.String] XML PRTG sensor output .NOTES TODO: Bug - Logic Flaw for Owner. Currently we search folders for non-inherited access rules, then we manually add a FullControl access rule for the Owner. This misses folders with only inherited access rules but a different owner. TODO: Bug - Doesn't work for AD users' default group/primary group (which is typically Domain Users). The user's default group is not listed in their memberOf attribute so I need to fix the LDAP search filter to include the primary group attribute. TODO: Bug - For a fake group created by New-FakeDirectoryEntry in the Adsi module, in the report its name will end up as an NT Account (CONTOSO\User123). If it is a fake user, its name will correctly appear without the domain prefix (User123) TODO: Feature - List any excluded accounts at the end TODO: Feature - Remove all usage of Add-Member to improve performance (create new pscustomobjects instead, nest original object inside) TODO: Feature - Parameter to specify properties to include in report TODO: Feature - This script does NOT account for individual file permissions. Only folder permissions are considered. TODO: Feature - This script does NOT account for file share permissions. Only NTFS permissions are considered. TODO: Feature - Support ACLs from Registry or AD objects TODO: Feature - psake task to update Release Notes in the script metadata to the github commit message .EXAMPLE .\Export-Permission.ps1 -TargetPath C:\Test Generate reports on the NTFS permissions for the folder C:\Test and all subfolders .EXAMPLE .\Export-Permission.ps1 -TargetPath C:\Test -AccountsToSkip 'BUILTIN\\Administrator' Generate reports on the NTFS permissions for the folder C:\Test and all subfolders Exclude the built-in Administrator account from the HTML report The AccountsToSkip parameter uses RegEx, so the \ in BUILTIN\Administrator needed to be escaped. The RegEx escape character is \ so that is why the regular expression needed for the parameter is 'BUILTIN\\Administrator' .EXAMPLE .\Export-Permission.ps1 -TargetPath C:\Test -ExcludeEmptyGroups Generate reports on the NTFS permissions for the folder C:\Test and all subfolders Exclude empty groups from the HTML report (leaving accounts only) .EXAMPLE .\Export-Permission.ps1 -TargetPath C:\Test -DomainToIgnore 'CONTOSO' Generate reports on the NTFS permissions for the folder C:\Test and all subfolders Remove the CONTOSO domain prefix from associated accounts and groups .EXAMPLE .\Export-Permission.ps1 -TargetPath C:\Test -LogDir C:\Logs Generate reports on the NTFS permissions for the folder C:\Test and all subfolders Redirect logs and output files to C:\Logs instead of the default location in AppData .EXAMPLE .\Export-Permission.ps1 -TargetPath C:\Test -LevelsOfSubfolders 0 Generate reports on the NTFS permissions for the folder C:\Test only (no subfolders) .EXAMPLE .\Export-Permission.ps1 -TargetPath C:\Test -LevelsOfSubfolders 2 Generate reports on the NTFS permissions for the folder C:\Test Only include subfolders to a maximum of 2 levels deep (C:\Test\Level1\Level2) .EXAMPLE .\Export-Permission.ps1 -TargetPath C:\Test -Title 'New Custom Report Title' Generate reports on the NTFS permissions for the folder C:\Test and all subfolders Change the title of the HTML report to 'New Custom Report Title' #> param ( # Path to the folder whose permissions to report (only tested with local paths, UNC may work, unknown) [string]$TargetPath = 'C:\Test', #[string]$TargetPath = '\\ad.contoso.com\coh\Test2\FolderWithoutTarget\FolderWithTarget\', # Regular expressions that will identify Users or Groups you do not want included in the Html report [string[]]$AccountsToSkip<# = @( 'BUILTIN\\Administrators', 'BUILTIN\\Administrator', 'CREATOR OWNER', 'NT AUTHORITY\\SYSTEM' )#>, # Exclude empty groups from the HTML report [switch]$ExcludeEmptyGroups, # Domains to ignore (they will be removed from the username) # Intended when a user has matching SamAccountNames in multiple domains but you only want them to appear once on the report. [string[]]$DomainToIgnore, # = @('CONTOSO1\\','CONTOSO2\\'), # Path to save the logs and reports generated by this script [string]$LogDir = "$env:AppData\Export-Permission\Logs", # Path containing the required modules for this script # Each module must match proper PowerShell module folder structure (module folder name matches the name of the .psm1 file) [string]$ModulesDir = '$PSScriptRoot\Modules', # Get group members [switch]$NoGroupMembers, <# How many levels of subfolder to enumerate Set to 0 to ignore all subfolders Set to -1 (default) to recurse infinitely Set to any whole number to enumerate that many levels #> [int]$LevelsOfSubfolders = -1, # Title at the top of the HTML report [string]$Title = "Folder Permissions Report", <# Valid group names that are allowed to appear in ACEs Specify as a ScriptBlock meant for the FilterScript parameter of Where-Object In the scriptblock, use string comparisons on the Name property e.g. {$_.Name -like 'CONTOSO\Group1*' -or $_.Name -eq 'CONTOSO\Group23'} The naming format that will be used for the groups is CONTOSO\Group1 where CONTOSO is the NetBIOS name of the domain, and Group1 is the samAccountName of the group By default, this is a scriptblock that always evaluates to $true so it doesn't evaluate any naming convention compliance #> [scriptblock]$GroupNamingConvention = { $true }, # If all four of the PRTG parameters are specified, # the results will be XML-formatted and pushed to the specified PRTG probe for a push sensor [string]$PrtgProbe, # If all four of the PRTG parameters are specified, # the results will be XML-formatted and pushed to the specified PRTG probe for a push sensor [string]$PrtgSensorProtocol, # If all four of the PRTG parameters are specified, # the results will be XML-formatted and pushed to the specified PRTG probe for a push sensor [int]$PrtgSensorPort, # If all four of the PRTG parameters are specified, # the results will be XML-formatted and pushed to the specified PRTG probe for a push sensor [string]$PrtgSensorToken ) #----------------[ Initialization ]---------------- # $PSScriptRoot is usually null inside the param block so I can't use the double-quotes up there to expand it # doing it this way allows comment-based help to accurately reflect the default values of these parameters if ($ModulesDir -eq '$PSScriptRoot\Modules') { $ModulesDir = "$PSScriptRoot\Modules" } #----------------[ Functions ]------------------ # Definition of Module 'Adsi' is below function Add-DomainFqdnToLdapPath { <# .SYNOPSIS Add a domain FQDN to an LDAP directory path as the server address so the new path can be used for remote queries .DESCRIPTION Uses RegEx to: Match the Domain Components from the Distinguished Name in the LDAP directory path Convert the Domain Components to an FQDN Insert them into the directory path as the server address .INPUTS [System.String]$DirectoryPath .OUTPUTS [System.String] Complete LDAP directory path including server address .EXAMPLE Add-DomainFqdnToLdapPath -DirectoryPath 'LDAP://CN=user1,OU=UsersOU,DC=ad,DC=contoso,DC=com' LDAP://ad.contoso.com/CN=user1,OU=UsersOU,DC=ad,DC=contoso,DC=com Add the domain FQDN to a single LDAP directory path #> [OutputType([System.String])] param ( # Incomplete LDAP directory path containing a distinguishedName but lacking a server address [Parameter(ValueFromPipeline)] [string[]]$DirectoryPath ) begin { $PathRegEx = '(?<Path>LDAP:\/\/[^\/]*)' $DomainRegEx = '(?i)DC=\w{1,}?\b' } process { ForEach ($ThisPath in $DirectoryPath) { if ($ThisPath -match $PathRegEx) { if ($ThisPath -match $DomainRegEx) { $DomainDN = $null $DomainFqdn = $null $DomainDN = ([regex]::Matches($ThisPath, $DomainRegEx) | ForEach-Object { $_.Value }) -join ',' $DomainFqdn = $DomainDN | ConvertTo-Fqdn if ($ThisPath -match "LDAP:\/\/$DomainFqdn\/") { #Write-Debug "Domain FQDN already found in the directory path: $($ThisPath)" $FQDNPath = $ThisPath } else { $FQDNPath = $ThisPath -replace 'LDAP:\/\/', "LDAP://$DomainFqdn/" } } else { #Write-Debug "Domain DN not found in the directory path: $($ThisPath)" $FQDNPath = $ThisPath } } else { #Write-Debug "Not an expected directory path: $($ThisPath)" $FQDNPath = $ThisPath } $FQDNPath } } } function Add-SidInfo { <# .SYNOPSIS Add some useful properties to a DirectoryEntry object for easier access .DESCRIPTION Add SidString, Domain, and SamAccountName NoteProperties to a DirectoryEntry .INPUTS [System.DirectoryServices.DirectoryEntry] or a [PSCustomObject] imitation. InputObject parameter. Must contain the objectSid property. .OUTPUTS [System.DirectoryServices.DirectoryEntry] or a [PSCustomObject] imitation. Whatever was input, but with three extra properties added now. .EXAMPLE [System.DirectoryServices.DirectoryEntry]::new('WinNT://localhost/Administrator') | Add-SidInfo distinguishedName : Path : WinNT://localhost/Administrator The output object's default format is not modified so with default formatting it appears identical to the original. Upon closer inspection it now has SidString, Domain, and SamAccountName properties. #> [OutputType([System.DirectoryServices.DirectoryEntry[]], [PSCustomObject[]])] param ( # Expecting a [System.DirectoryServices.DirectoryEntry] from the LDAP or WinNT providers, or a [PSCustomObject] imitation from Get-DirectoryEntry. # Must contain the objectSid property [Parameter(ValueFromPipeline)] $InputObject, <# Hashtable containing cached directory entries so they don't have to be retrieved from the directory again Uses a thread-safe hashtable by default #> [hashtable]$DirectoryEntryCache = ([hashtable]::Synchronized(@{})), # Hashtable containing known domain SIDs as the keys and their names as the values $TrustedDomainSidNameMap = (Get-TrustedDomainSidNameMap -DirectoryEntryCache $DirectoryEntryCache) ) begin {} process { ForEach ($Object in $InputObject) { $SID = $null if ($null -eq $Object) { continue } elseif ( $null -ne $Object.objectSid.Value -and # With WinNT directory entries for the root (WinNT://localhost), objectSid is a method rather than a property # So we need to filter out those instances here to avoid this error: # The following exception occurred while retrieving the string representation for method "objectSid": # "Object reference not set to an instance of an object." $Object.objectSid.Value.GetType().FullName -ne 'System.Management.Automation.PSMethod' ) { [string]$SID = [System.Security.Principal.SecurityIdentifier]::new([byte[]]$Object.objectSid.Value, 0) } elseif ($Object.Properties['objectSid'].Value) { [string]$SID = [System.Security.Principal.SecurityIdentifier]::new([byte[]]$Object.Properties['objectSid'].Value, 0) } elseif ($Object.Properties['objectSid']) { [string]$SID = [System.Security.Principal.SecurityIdentifier]::new([byte[]]($Object.Properties['objectSid'] | ForEach-Object { $_ }), 0) } elseif ($Object.objectSid) { [string]$SID = [System.Security.Principal.SecurityIdentifier]::new([byte[]]$Object.objectSid, 0) } if ($Object.Properties['samaccountname']) { $SamAccountName = $Object.Properties['samaccountname'] } else { #DirectoryEntries from the WinNT provider for local accounts do not have a samaccountname attribute so we use name instead $SamAccountName = $Object.Properties['name'] } $DomainObject = $null if ($Object.Domain.Sid) { #if ($Object.Domain.GetType().FullName -ne 'System.Management.Automation.PSMethod') { # This would only have come from Add-SidInfo in the first place # This means it was added with Add-Member in Get-DirectoryEntry for the root of the computer's directory if ($null -eq $SID) { [string]$SID = $Object.Domain.Sid } $DomainObject = $Object.Domain #} } if (!($DomainObject)) { # The SID of the domain is the SID of the user minus the last block of numbers $DomainSid = $SID.Substring(0, $Sid.LastIndexOf("-")) # Lookup other information about the domain using its SID as the key $DomainObject = $TrustedDomainSidNameMap[$DomainSid] } #Write-Debug "$SamAccountName`t$SID" $Object | Add-Member -PassThru -Force @{ SidString = $SID Domain = $DomainObject SamAccountName = $SamAccountName } } } end { } } function ConvertFrom-PropertyValueCollectionToString { <# .SYNOPSIS Convert a PropertyValueCollection to a string .DESCRIPTION Useful when working with System.DirectoryServices and some other namespaces .INPUTS None. Pipeline input is not accepted. .OUTPUTS [System.String] .EXAMPLE $DirectoryEntry = [adsi]("WinNT://$(hostname)") $DirectoryEntry.Properties.Keys | ForEach-Object { ConvertFrom-PropertyValueCollectionToString -PropertyValueCollection $DirectoryEntry.Properties[$_] } For each property in a DirectoryEntry, convert its corresponding PropertyValueCollection to a string #> param ( [System.DirectoryServices.PropertyValueCollection]$PropertyValueCollection ) $SubType = & { $PropertyValueCollection.Value.GetType().FullName } 2>$null switch ($SubType) { 'System.Byte[]' { ConvertTo-DecStringRepresentation -ByteArray $PropertyValueCollection.Value } default { "$($PropertyValueCollection.Value)" } } } function ConvertTo-DecStringRepresentation { <# .SYNOPSIS Convert a byte array to a string representation of its decimal format .DESCRIPTION Uses the custom format operator -f to format each byte as a string decimal representation .INPUTS [System.Byte[]]$ByteArray .OUTPUTS [System.String] Array of strings representing the byte array's decimal values .EXAMPLE ConvertTo-DecStringRepresentation -ByteArray $Bytes Convert the binary SID $Bytes to a decimal string representation #> [OutputType([System.String])] param ( # Byte array. Often the binary format of an objectSid or LoginHours [byte[]]$ByteArray ) $ByteArray | ForEach-Object { '{0}' -f $_ } } function ConvertTo-DistinguishedName { <# .SYNOPSIS Convert a domain NetBIOS name to its distinguishedName .DESCRIPTION https://docs.microsoft.com/en-us/windows/win32/api/iads/nn-iads-iadsnametranslate .INPUTS [System.String]$Domain .OUTPUTS [System.String] distinguishedName of the domain .EXAMPLE ConvertTo-DistinguishedName -Domain 'CONTOSO' DC=ad,DC=contoso,DC=com Resolve the NetBIOS domain 'CONTOSO' to its distinguishedName 'DC=ad,DC=contoso,DC=com' #> [OutputType([System.String])] param ( # NetBIOS name of the domain [Parameter(Mandatory, ValueFromPipeline)] [string[]]$Domain ) process { ForEach ($ThisDomain in $Domain) { $IADsNameTranslateComObject = New-Object -comObject "NameTranslate" $IADsNameTranslateInterface = $IADsNameTranslateComObject.GetType() $null = $IADsNameTranslateInterface.InvokeMember("Init", "InvokeMethod", $Null, $IADsNameTranslateComObject, (3, $Null)) $null = $IADsNameTranslateInterface.InvokeMember("Set", "InvokeMethod", $Null, $IADsNameTranslateComObject, (3, "$ThisDomain\")) $IADsNameTranslateInterface.InvokeMember("Get", "InvokeMethod", $Null, $IADsNameTranslateComObject, 1) } } } function ConvertTo-Fqdn { <# .SYNOPSIS Convert a domain distinguishedName name to its FQDN .DESCRIPTION Uses PowerShell's -replace operator to perform the conversion .INPUTS [System.String]$DistinguishedName .OUTPUTS [System.String] FQDN version of the distinguishedName .EXAMPLE ConvertTo-Fqdn -DistinguishedName 'DC=ad,DC=contoso,DC=com' ad.contoso.com Convert the domain distinguishedName 'DC=ad,DC=contoso,DC=com' to its FQDN format 'ad.contoso.com' #> [OutputType([System.String])] param ( # distinguishedName of the domain [Parameter(ValueFromPipeline)] [string[]]$DistinguishedName ) process { ForEach ($DN in $DistinguishedName) { $DN -replace ',DC=', '.' -replace 'DC=', '' } } } function ConvertTo-HexStringRepresentation { <# .SYNOPSIS Convert a SID from byte array format to a string representation of its hexadecimal format .DESCRIPTION Uses the custom format operator -f to format each byte as a string hex representation .INPUTS [System.Byte[]]$SIDByteArray .OUTPUTS [System.String] SID as an array of strings representing the byte array's hexadecimal values .EXAMPLE ConvertTo-HexStringRepresentation -SIDByteArray $Bytes Convert the binary SID $Bytes to a hexadecimal string representation #> [OutputType([System.String[]])] param ( # SID [byte[]]$SIDByteArray ) $SIDHexString = $SIDByteArray | ForEach-Object { '{0:X}' -f $_ } return $SIDHexString } function ConvertTo-HexStringRepresentationForLDAPFilterString { <# .SYNOPSIS Convert a SID from byte array format to a string representation of its hexadecimal format, properly formatted for an LDAP filter string .DESCRIPTION Uses the custom format operator -f to format each byte as a string hex representation .INPUTS [System.Byte[]]$SIDByteArray .OUTPUTS [System.String] SID as an array of strings representing the byte array's hexadecimal values .EXAMPLE ConvertTo-HexStringRepresentationForLDAPFilterString -SIDByteArray $Bytes Convert the binary SID $Bytes to a hexadecimal string representation, formatted for use in an LDAP filter string #> [OutputType([System.String])] param ( # SID to convert to a hex string [byte[]]$SIDByteArray ) $Hexes = $SIDByteArray | ForEach-Object { '{0:X}' -f $_ } | ForEach-Object { if ($_.Length -eq 2) { $_ } else { "0$_" } } "\$($Hexes -join '\')" } function ConvertTo-SidByteArray { <# .SYNOPSIS Convert a SID from a string to binary format (byte array) .DESCRIPTION Uses the GetBinaryForm method of the [System.Security.Principal.SecurityIdentifier] class .INPUTS [System.String]$SidString .OUTPUTS [System.Byte] SID a a byte array .EXAMPLE ConvertTo-SidByteArray -SidString $SID Convert the SID string to a byte array #> [OutputType([System.Byte[]])] param ( # SID to convert to binary [Parameter(ValueFromPipeline)] [string[]]$SidString ) process { ForEach ($ThisSID in $SidString) { $SID = [System.Security.Principal.SecurityIdentifier]::new($ThisSID) [byte[]]$Bytes = [byte[]]::new($SID.BinaryLength) $SID.GetBinaryForm($Bytes, 0) $Bytes } } } function Expand-AdsiGroupMember { <# .SYNOPSIS Use the LDAP provider to add information about group members to a DirectoryEntry of a group for easier access .DESCRIPTION Recursively retrieves group members and detailed information about them .INPUTS [System.DirectoryServices.DirectoryEntry]$DirectoryEntry .OUTPUTS [System.DirectoryServices.DirectoryEntry] Returned with member info added now (if the DirectoryEntry is a group). .EXAMPLE [System.DirectoryServices.DirectoryEntry]::new('WinNT://localhost/Administrators') | Get-AdsiGroupMember | Expand-AdsiGroupMember Need to fix example and add notes #> [OutputType([System.DirectoryServices.DirectoryEntry])] param ( # Expecting a DirectoryEntry from the LDAP or WinNT providers, or a PSObject imitation from Get-DirectoryEntry [parameter(ValueFromPipeline)] $DirectoryEntry, # Properties of the group members to retrieve [string[]]$PropertiesToLoad = @('operatingSystem', 'objectSid', 'samAccountName', 'objectClass', 'distinguishedName', 'name', 'grouptype', 'description', 'managedby', 'member', 'objectClass', 'Department', 'Title'), <# Hashtable containing cached directory entries so they don't need to be retrieved from the directory again Uses a thread-safe hashtable by default #> [hashtable]$DirectoryEntryCache = ([hashtable]::Synchronized(@{})), # Hashtable containing known domain SIDs as the keys and their names as the values $TrustedDomainSidNameMap = (Get-TrustedDomainSidNameMap -DirectoryEntryCache $DirectoryEntryCache) ) begin { $i = 0 } process { ForEach ($Entry in $DirectoryEntry) { $i++ #$status = ("$(Get-Date -Format s)`t$(hostname)`tExpand-AdsiGroupMember`tStatus: Using ADSI to get info on group member $i`: " + $Entry.Name) #Write-Debug " $status" $Principal = $null if ($Entry.objectClass -contains 'foreignSecurityPrincipal') { if ($Entry.distinguishedName.Value -match '(?>^CN=)(?<SID>[^,]*)') { [string]$SID = $Matches.SID #The SID of the domain is the SID of the user minus the last block of numbers $DomainSid = $SID.Substring(0, $Sid.LastIndexOf("-")) $Domain = $TrustedDomainSidNameMap[$DomainSid] #$Success = $true #try { $Principal = Get-DirectoryEntry -DirectoryPath "LDAP://$($Domain.Dns)/<SID=$SID>" -DirectoryEntryCache $DirectoryEntryCache #} catch { # $Success = $false # $Principal = $Entry # Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-AdsiGroupMember`t$SID could not be retrieved from $Domain" #} #if ($Success -eq $true) { try { $null = $Principal.RefreshCache($PropertiesToLoad) } catch { #$Success = $false $Principal = $Entry Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-AdsiGroupMember`t$SID could not be retrieved from $Domain" } # Recursively enumerate group members if ($Principal.properties['objectClass'].Value -contains 'group') { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-AdsiGroupMember`t'$($Principal.properties['name'])' is a group in $Domain" $Principal = ($Principal | Get-AdsiGroupMember -DirectoryEntryCache $DirectoryEntryCache).FullMembers | Expand-AdsiGroupMember -DirectoryEntryCache $DirectoryEntryCache -TrustedDomainSidNameMap $TrustedDomainSidNameMap } #} } } else { $Principal = $Entry } $Principal | Add-SidInfo -DirectoryEntryCache $DirectoryEntryCache -TrustedDomainSidNameMap $TrustedDomainSidNameMap } } } function Expand-IdentityReference { <# .SYNOPSIS Use ADSI to collect more information about the IdentityReference in NTFS Access Control Entries .DESCRIPTION Recursively retrieves group members and detailed information about them Use caching to reduce duplicate directory queries .INPUTS [System.Object]$AccessControlEntry .OUTPUTS [System.Object] The input object is returned with additional properties added: DirectoryEntry DomainDn DomainNetBIOS ObjectType Members (if the DirectoryEntry is a group). .EXAMPLE (Get-Acl).Access | Resolve-IdentityReference | Group-Object -Property IdentityReferenceResolved | Expand-IdentityReference Incomplete example but it shows the chain of functions to generate the expected input for this #> [OutputType([System.Object])] param ( # The NTFS AccessControlEntry object(s), grouped by their IdentityReference property # TODO: Use System.Security.Principal.NTAccount instead [Parameter(ValueFromPipeline)] [System.Object[]]$AccessControlEntry, # Do not get group members [switch]$NoGroupMembers, # Thread-safe hashtable to use for caching directory entries and avoiding duplicate directory queries [hashtable]$DirectoryEntryCache = ([hashtable]::Synchronized(@{})), # Thread-safe hashtable to use for caching directory entries and avoiding duplicate directory queries [hashtable]$IdentityReferenceCache = ([hashtable]::Synchronized(@{})) ) begin { #Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`t$(($AccessControlEntry | Measure).Count) unique IdentityReferences found in the $(($AccessControlEntry | Measure).Count) ACEs" # Get the SID of the current domain $CurrentDomain = (Get-CurrentDomain) # Convert the objectSID attribute (byte array) to a security descriptor string formatted according to SDDL syntax (Security Descriptor Definition Language) [string]$CurrentDomainSID = & { [System.Security.Principal.SecurityIdentifier]::new([byte[]]$CurrentDomain.objectSid.Value, 0) } 2>$null $KnownDomains = @{} $i = 0 } process { ForEach ($ThisIdentity in $AccessControlEntry) { $ThisIdentityGroup = $ThisIdentity.Group $i++ #Calculate the completion percentage, and format it to show 0 decimal places $percentage = "{0:N0}" -f (($i / ($AccessControlEntry.Count)) * 100) #Display the progress bar $status = $percentage + "% - Using ADSI to get info on NTFS IdentityReference $i of " + $AccessControlEntry.Count + ": " + $ThisIdentity.Name #Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`tStatus: $status" #Write-Progress -Activity ("Unique IdentityReferences: " + $AccessControlEntry.Count) -Status $status -PercentComplete $percentage if ($null -eq $IdentityReferenceCache[$ThisIdentity.Name]) { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`tIdentityReferenceCache miss for '$($ThisIdentity.Name)'" $DomainDN = $null $DirectoryEntry = $null $Members = $null $StartingIdentityName = $ThisIdentity.Name $split = $StartingIdentityName.Split('\') $domainNetbiosString = $split[0] $name = $split[1] if ( $null -ne $name -and ($ThisIdentity.Group.AdsiProvider | Select-Object -First 1) -eq 'LDAP' ) { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`t$($StartingIdentityName) is a domain security principal" # Add this domain to our list of known domains if (!($KnownDomains[$domainNetbiosString])) { $KnownDomains[$domainNetbiosString] = ConvertTo-DistinguishedName -Domain $domainNetbiosString Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`tCache miss for domain $($domainNetbiosString). Adding its Distinguished Name to dictionary of known domains for future lookup" } # Search the domain for the principal $DomainDn = $KnownDomains[$domainNetbiosString] try { $SearchPath = "LDAP://$DomainDn" | Add-DomainFqdnToLdapPath $DirectoryEntry = Search-Directory -DirectoryEntryCache $DirectoryEntryCache -DirectoryPath $SearchPath -Filter "(samaccountname=$Name)" -PropertiesToLoad @('objectClass', 'distinguishedName', 'name', 'grouptype', 'description', 'managedby', 'member', 'objectClass', 'Department', 'Title') } catch { Write-Warning "$(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`t$($StartingIdentityName) could not be resolved against its directory" Write-Warning "$(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`t$($_.Exception.Message)" } } elseif (((($StartingIdentityName -split '-') | Select-Object -SkipLast 1) -join '-') -eq $CurrentDomainSID) { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`t$($StartingIdentityName) is an unresolved SID from the current domain" # Get the distinguishedName and netBIOSName of the current domain. This also determines whether the domain is online. $DomainDN = $CurrentDomain.distinguishedName.Value $DomainFQDN = $DomainDN | ConvertTo-Fqdn $PartitionsPath = "LDAP://cn=partitions,cn=configuration,$DomainDn" | Add-DomainFqdnToLdapPath $DomainCrossReference = Search-Directory -DirectoryEntryCache $DirectoryEntryCache -DirectoryPath $PartitionsPath -Filter "(&(objectcategory=crossref)(dnsroot=$DomainFQDN)(netbiosname=*))" -PropertiesToLoad netbiosname if ($DomainCrossReference.Properties ) { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`tThe domain '$DomainFQDN' is online" $domainNetbiosString = $DomainCrossReference.Properties['netbiosname'] # TODO: The domain is online, so let's see if any domain trusts have issues? Determine if SID is foreign security principal? # TODO: What if the foreign security principal exists but the corresponding domain trust is down? Don't want to recommend deletion of the ACE in that case. } $SidObject = [System.Security.Principal.SecurityIdentifier]::new($StartingIdentityName) $SidBytes = [byte[]]::new($SidObject.BinaryLength) $null = $SidObject.GetBinaryForm($SidBytes, 0) $ObjectSid = ConvertTo-HexStringRepresentationForLDAPFilterString -SIDByteArray $SidBytes try { $DirectoryEntry = Search-Directory -DirectoryEntryCache $DirectoryEntryCache -DirectoryPath "LDAP://$DomainDn" -Filter "(objectsid=$ObjectSid)" -PropertiesToLoad @('objectClass', 'distinguishedName', 'name', 'grouptype', 'description', 'managedby', 'member', 'objectClass', 'Department', 'Title') } catch { Write-Warning "$(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`t$($StartingIdentityName) could not be resolved against its directory" Write-Warning "$(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`t$($_.Exception.Message)" } } else { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`t$($StartingIdentityName) is a local security principal or unresolved SID" # Determine if SID belongs to current domain $IdentityDomainSID = (($StartingIdentityName -split '-') | Select-Object -SkipLast 1) -join '-' if ($IdentityDomainSID -eq $CurrentDomainSID) { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`t$($StartingIdentityName) belongs to the current domain. Could be a deleted user. ?possibly a foreign security principal corresponding to an offline trusted domain or deleted user in the trusted domain?" } else { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`t$($StartingIdentityName) does not belong to the current domain. Could be a local security principal or belong to an unresolvable domain." } if ($null -eq $name) { $name = $StartingIdentityName } if ($name -match 'S-\d+-\d+-\d+-\d+-\d+\-\d+\-\d+') { if ($Domains.Count -gt 1) { $DirectoryEntry = ForEach ($domainNetbiosString in $Domains) { try { $UsersGroup = Get-DirectoryEntry -DirectoryPath "WinNT://$domainNetbiosString/Users,group" -DirectoryEntryCache $DirectoryEntryCache } catch { Write-Warning "$(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`tCould not connect to $domainNetbiosString using PSRemoting" Write-Warning "$(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`t$_" } $MembersOfUsersGroup = Get-WinNTGroupMember -DirectoryEntry $UsersGroup -DirectoryEntryCache $DirectoryEntryCache $MembersOfUsersGroup | Where-Object -FilterScript { ($name -eq [System.Security.Principal.SecurityIdentifier]::new([byte[]]$_.Properties['objectSid'].Value, 0)) } $ThisIdentity = [pscustomobject]@{ Count = $(($ThisIdentityGroup | Measure-Object).Count) Name = "$domainNetbiosString\" + $DirectoryEntry.Name Group = $ThisIdentityGroup | Where-Object -FilterScript { ($_.SourceAccessList.Path -split '\\')[2] -eq $domainNetbiosString } #####Group = $ThisIdentityGroup | Where-Object -FilterScript { ($_.Path -split '\\')[2] -eq $domainNetbiosString } } } } else { try { $UsersGroup = Get-DirectoryEntry -DirectoryPath "WinNT://$domainNetbiosString/Users,group" -DirectoryEntryCache $DirectoryEntryCache } catch { Write-Warning "$(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`tCould not connect to $domainNetbiosString using PSRemoting" Write-Warning "$(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`t$_" } $MembersOfUsersGroup = Get-WinNTGroupMember -DirectoryEntry $UsersGroup -DirectoryEntryCache $DirectoryEntryCache $DirectoryEntry = $MembersOfUsersGroup | Where-Object -FilterScript { ($name -eq [System.Security.Principal.SecurityIdentifier]::new([byte[]]$_.Properties['objectSid'].Value, 0)) } $ThisIdentity = [pscustomobject]@{ Count = $(($ThisIdentityGroup | Measure-Object).Count) Name = "$domainNetbiosString\" + $DirectoryEntry.Name Group = $ThisIdentityGroup } } } else { if ($Domains.Count -gt 1) { $DirectoryEntry = ForEach ($domainNetbiosString in $Domains) { $DirectoryPath = "WinNT://$domainNetbiosString/$name" try { Get-DirectoryEntry -DirectoryPath $DirectoryPath -PropertiesToLoad members -DirectoryEntryCache $DirectoryEntryCache } catch { Write-Warning "$(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`t$($DirectoryPath) could not be resolved" } } } else { $DirectoryPath = "WinNT://$domainNetbiosString/$name" try { $DirectoryEntry = Get-DirectoryEntry -DirectoryPath $DirectoryPath -PropertiesToLoad members -DirectoryEntryCache $DirectoryEntryCache } catch { Write-Warning "$(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`t$($DirectoryPath) could not be resolved" } } } } $ObjectType = $null if ($null -ne $DirectoryEntry) { $ThisIdentity | Add-Member -Name 'DirectoryEntry' -Value $DirectoryEntry -MemberType NoteProperty -Force if ( $DirectoryEntry.Properties['objectClass'] -contains 'group' -or $DirectoryEntry.SchemaClassName -contains 'Group' ) { $ObjectType = 'Group' } else { $ObjectType = 'User' } if ($NoGroupMembers -eq $false) { if ($DirectoryEntry.Properties['objectClass'] -contains 'group') { # Retrieve the members of groups from the LDAP provider #$Members = (Get-AdsiGroup -DirectoryEntryCache $DirectoryEntryCache -DirectoryPath $DirectoryEntry.Path).FullMembers $Members = Get-AdsiGroupMember -Group $DirectoryEntry -DirectoryEntryCache $DirectoryEntryCache } else { # Retrieve the members of groups from the WinNT provider Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`t$($DirectoryEntry.Path) must be a WinNT user or group" $Members = Get-WinNTGroupMember -DirectoryEntryCache $DirectoryEntryCache -DirectoryEntry $DirectoryEntry -KnownDomains $KnownDomains } if ($Members) { $Members | ForEach-Object { if ($_.Domain) { $_ | Add-Member -Force -NotePropertyMembers @{ Group = $ThisIdentityGroup } } else { $_ | Add-Member -Force -NotePropertyMembers @{ Group = $ThisIdentityGroup Domain = [pscustomobject]@{ Dns = $domainNetbiosString Netbios = $domainNetbiosString Sid = ($name -split '-') | Select-Object -Last 1 } } } } } Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`t$($DirectoryEntry.Path) has $(($Members | Measure-Object).Count) members" $ThisIdentity | Add-Member -Name 'Members' -Value $Members -MemberType NoteProperty -Force } } else { Write-Warning "$(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`t$($StartingIdentityName) could not be matched to a DirectoryEntry" } $ThisIdentity | Add-Member -Force -NotePropertyMembers @{ DomainDn = $DomainDn DomainNetbios = $DomainNetBiosString ObjectType = $ObjectType } $IdentityReferenceCache[$StartingIdentityName] = $ThisIdentity } else { #Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`tIdentityReferenceCache hit for '$($ThisIdentity.Name)'" $null = $IdentityReferenceCache[$ThisIdentity.Name].Group.Add($ThisIdentityGroup) $ThisIdentity = $IdentityReferenceCache[$ThisIdentity.Name] } $ThisIdentity } } end { #Write-Progress -Activity Completed -Completed } } function Expand-WinNTGroupMember { <# .SYNOPSIS Use the LDAP provider to add information about group members to a DirectoryEntry of a group for easier access .DESCRIPTION Recursively retrieves group members and detailed information about them .INPUTS [System.DirectoryServices.DirectoryEntry]$DirectoryEntry .OUTPUTS [System.DirectoryServices.DirectoryEntry] Returned with member info added now (if the DirectoryEntry is a group). .EXAMPLE [System.DirectoryServices.DirectoryEntry]::new('WinNT://localhost/Administrators') | Get-WinNTGroupMember | Expand-WinNTGroupMember Need to fix example and add notes #> [OutputType([System.DirectoryServices.DirectoryEntry])] param ( # Expecting a DirectoryEntry from the WinNT provider, or a PSObject imitation from Get-DirectoryEntry [Parameter(ValueFromPipeline)] $DirectoryEntry, <# Hashtable containing cached directory entries so they don't need to be retrieved from the directory again Uses a thread-safe hashtable by default #> [hashtable]$DirectoryEntryCache = ([hashtable]::Synchronized(@{})) ) begin {} process { ForEach ($ThisEntry in $DirectoryEntry) { if (!($ThisEntry.Properties)) { Write-Warning "'$ThisEntry' has no properties" } elseif ($ThisEntry.Properties['objectClass'] -contains 'group') { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-WinNTGroupMember`t'$($ThisEntry.Path)' is an ADSI group" (Get-AdsiGroup -DirectoryEntryCache $DirectoryEntryCache -DirectoryPath $ThisEntry.Path).FullMembers | Add-SidInfo -DirectoryEntryCache $DirectoryEntryCache } else { if ($ThisEntry.SchemaClassName -contains 'group') { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-WinNTGroupMember`t'$($ThisEntry.Path)' is a WinNT group" if ($ThisEntry.GetType().FullName -eq 'System.Collections.Hashtable') { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-WinNTGroupMember`t'$($ThisEntry.Path)' is a special group with no direct memberships" $ThisEntry | Add-SidInfo -DirectoryEntryCache $DirectoryEntryCache } else { Get-WinNTGroupMember -DirectoryEntry $ThisEntry -DirectoryEntryCache $DirectoryEntryCache } } else { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-WinNTGroupMember`t'$($ThisEntry.Path)' is a user account" $ThisEntry | Add-SidInfo -DirectoryEntryCache $DirectoryEntryCache } } } } end {} } function Find-AdsiProvider { <# .SYNOPSIS Determine whether a directory server is an LDAP or a WinNT server .DESCRIPTION Uses the ADSI provider to attempt to query the server using LDAP first, then WinNT second .INPUTS [System.String] AdsiServer parameter. .OUTPUTS [System.String] Possible return values are: None LDAP WinNT .EXAMPLE Find-AdsiProvider -AdsiServer localhost Find the ADSI provider of the local computer .EXAMPLE Find-AdsiProvider -AdsiServer 'ad.contoso.com' Find the ADSI provider of the AD domain 'ad.contoso.com' #> [OutputType([System.String])] param ( # IP address or hostname of the directory server whose ADSI provider type to determine [Parameter(ValueFromPipeline)] [string[]]$AdsiServer ) process { ForEach ($ThisServer in $AdsiServer) { $AdsiProvider = $null $AdsiPath = "LDAP://$ThisServer" Write-Debug " $(Get-Date -Format s)`t$(hostname)`tFind-AdsiProvider`t[System.DirectoryServices.DirectoryEntry]::Exists('$AdsiPath')" try { $null = [System.DirectoryServices.DirectoryEntry]::Exists($AdsiPath) $AdsiProvider = 'LDAP' } catch { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tFind-AdsiProvider`t$ThisServer is not an LDAP server" } if (!$AdsiProvider) { $AdsiPath = "WinNT://$ThisServer" Write-Debug " $(Get-Date -Format s)`t$(hostname)`tFind-AdsiProvider`t[System.DirectoryServices.DirectoryEntry]::Exists('$AdsiPath')" try { $null = [System.DirectoryServices.DirectoryEntry]::Exists($AdsiPath) $AdsiProvider = 'WinNT' } catch { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tFind-AdsiProvider`t$ThisServer is not a WinNT server" } } if (!$AdsiProvider) { $AdsiProvider = 'none' } } $AdsiProvider } } function Find-ServerNameInPath { <# .SYNOPSIS Parse a literal path to find its server .DESCRIPTION Currently only supports local file paths or UNC paths .INPUTS None. Pipeline input is not accepted. .OUTPUTS [System.String] representing the name of the server that was extracted from the path .EXAMPLE Find-ServerNameInPath -LiteralPath 'C:\Test' Return the hostname of the local computer because a local filepath was used .EXAMPLE Find-ServerNameInPath -LiteralPath '\\server123\Test\' Return server123 because a UNC path for a folder shared on server123 was used #> [OutputType([System.String])] param ( [string]$LiteralPath ) if ($LiteralPath -match '[A-Za-z]\:\\' -or $null -eq $LiteralPath -or '' -eq $LiteralPath) { # For local file paths, the "server" is the local computer. Assume the same for null paths. hostname } else { # Otherwise it must be a UNC path, so the server is the first non-empty string between backwhacks (\) $ThisServer = $LiteralPath -split '\\' | Where-Object -FilterScript { $_ -ne '' } | Select-Object -First 1 $ThisServer -replace '\?', (hostname) } } function Get-AdsiGroup { <# .SYNOPSIS Get the directory entries for a group and its members using ADSI .DESCRIPTION Uses the ADSI components to search a directory for a group, then get its members Both the WinNT and LDAP providers are supported .INPUTS None. Pipeline input is not accepted. .OUTPUTS [System.DirectoryServices.DirectoryEntry] for each group memeber .EXAMPLE Get-AdsiGroup -DirectoryPath 'WinNT://WORKGROUP/localhost' -GroupName Administrators Get members of the local Administrators group .EXAMPLE Get-AdsiGroup -GroupName Administrators On a domain-joined computer, this will get members of the domain's Administrators group On a workgroup computer, this will get members of the local Administrators group #> [OutputType([System.DirectoryServices.DirectoryEntry])] param ( <# Path to the directory object to retrieve Defaults to the root of the current domain #> [string]$DirectoryPath = (([System.DirectoryServices.DirectorySearcher]::new()).SearchRoot.Path), # Name (CN or Common Name) of the group to retrieve [string]$GroupName, # Properties of the group and its members to find in the directory <# [string[]]$PropertiesToLoad = @( 'department', 'description', 'distinguishedName', 'grouptype', 'managedby', 'member', 'name', 'objectClass', 'objectSid', 'operatingSystem', 'samAccountName', 'title' ), #> [string[]]$PropertiesToLoad, <# Hashtable containing cached directory entries so they don't have to be retrieved from the directory again Uses a thread-safe hashtable by default #> [hashtable]$DirectoryEntryCache = ([hashtable]::Synchronized(@{})) ) $GroupParams = @{ DirectoryEntryCache = $DirectoryEntryCache DirectoryPath = $DirectoryPath PropertiesToLoad = $PropertiesToLoad } $GroupMemberParams = @{ DirectoryEntryCache = $DirectoryEntryCache PropertiesToLoad = $PropertiesToLoad } switch -Regex ($DirectoryPath) { '^WinNT' { $GroupParams['DirectoryPath'] = "$DirectoryPath/$GroupName" Get-DirectoryEntry @GroupParams | Get-WinNTGroupMember @GroupMemberParams } '^$' { # This is expected for a workgroup computer $GroupParams['DirectoryPath'] = "WinNT://localhost/$GroupName" Get-DirectoryEntry @GroupParams | Get-WinNTGroupMember @GroupMemberParams } default { if ($GroupName) { $GroupParams['Filter'] = "(&(objectClass=group)(cn=$GroupName))" } else { $GroupParams['Filter'] = "(objectClass=group)" } Search-Directory @GroupParams | Get-AdsiGroupMember @GroupMemberParams } } } function Get-AdsiGroupMember { <# .SYNOPSIS Get members of a group from the LDAP provider .DESCRIPTION Use ADSI to get members of a group from the LDAP provider Return the group's DirectoryEntry plus a FullMembers property containing the member DirectoryEntries .INPUTS [System.DirectoryServices.DirectoryEntry]$DirectoryEntry .OUTPUTS [System.DirectoryServices.DirectoryEntry] plus a FullMembers property .EXAMPLE [System.DirectoryServices.DirectoryEntry]::new('LDAP://ad.contoso.com/CN=Administrators,CN=BuiltIn,DC=ad,DC=contoso,DC=com') | Get-AdsiGroupMember Get members of the domain Administrators group #> [OutputType([System.DirectoryServices.DirectoryEntry])] param ( # Directory entry of the LDAP group whose members to get [Parameter(ValueFromPipeline)] $Group, # Properties of the group members to find in the directory [string[]]$PropertiesToLoad, <# Hashtable containing cached directory entries so they don't have to be retrieved from the directory again Uses a thread-safe hashtable by default #> [hashtable]$DirectoryEntryCache = ([hashtable]::Synchronized(@{})) ) begin { $PathRegEx = '(?<Path>LDAP:\/\/[^\/]*)' $DomainRegEx = '(?i)DC=\w{1,}?\b' $SearchParameters = @{ PropertiesToLoad = $PropertiesToLoad DirectoryEntryCache = $DirectoryEntryCache } $TrustedDomainSidNameMap = Get-TrustedDomainSidNameMap -DirectoryEntryCache $DirectoryEntryCache } process { foreach ($ThisGroup in $Group) { # Recursive search $SearchParameters['Filter'] = "(memberof:1.2.840.113556.1.4.1941:=$($ThisGroup.Properties['distinguishedname']))" # Non-recursive search #$SearchParameters['Filter'] = "(memberof=$($ThisGroup.Properties['distinguishedname']))" if ($ThisGroup.Path -match $PathRegEx) { $SearchParameters['DirectoryPath'] = $Matches.Path | Add-DomainFqdnToLdapPath if ($ThisGroup.Path -match $DomainRegEx) { $Domain = ([regex]::Matches($ThisGroup.Path, $DomainRegEx) | ForEach-Object { $_.Value }) -join ',' $SearchParameters['DirectoryPath'] = "LDAP://$Domain" | Add-DomainFqdnToLdapPath } else { $SearchParameters['DirectoryPath'] = $ThisGroup.Path | Add-DomainFqdnToLdapPath } } else { $SearchParameters['DirectoryPath'] = $ThisGroup.Path | Add-DomainFqdnToLdapPath } #Write-Debug " $(Get-Date -Format s)`t$(hostname)`tGet-AdsiGroupMember`t$($SearchParameters['Filter'])" $GroupMemberSearch = Search-Directory @SearchParameters if ($GroupMemberSearch.Count -gt 0) { $CurrentADGroupMembers = $GroupMemberSearch | ForEach-Object { $FQDNPath = $_.Path | Add-DomainFqdnToLdapPath Get-DirectoryEntry -DirectoryPath $FQDNPath -DirectoryEntryCache $DirectoryEntryCache } } else { $CurrentADGroupMembers = $null } Write-Debug " $(Get-Date -Format s)`t$(hostname)`tGet-AdsiGroupMember`t$($ThisGroup.Properties.name) has $(($CurrentADGroupMembers | Measure-Object).Count) members" $ProcessedGroupMembers = $CurrentADGroupMembers | Expand-AdsiGroupMember -DirectoryEntryCache $DirectoryEntryCache -TrustedDomainSidNameMap $TrustedDomainSidNameMap $ThisGroup | Add-Member -MemberType NoteProperty -Name FullMembers -Value $ProcessedGroupMembers -Force -PassThru } } end {} } function Get-AdsiServer { <# .SYNOPSIS Get information about a directory server including the ADSI provider it hosts and its well-known SIDs .DESCRIPTION Uses the ADSI provider to query the server using LDAP first, then WinNT upon failure Uses WinRM to query the CIM class Win32_SystemAccount for well-known SIDs .INPUTS [System.String]$AdsiServer .OUTPUTS [PSCustomObject] with AdsiProvider and WellKnownSIDs properties .EXAMPLE Get-AdsiServer -AdsiServer localhost Find the ADSI provider of the local computer .EXAMPLE Get-AdsiServer -AdsiServer 'ad.contoso.com' Find the ADSI provider of the AD domain 'ad.contoso.com' #> [OutputType([System.String])] param ( # IP address or hostname of the directory server whose ADSI provider type to determine [Parameter(ValueFromPipeline)] [string[]]$AdsiServer, # Cache of known directory servers to reduce duplicate queries [hashtable]$KnownServers = [hashtable]::Synchronized(@{}) ) process { ForEach ($ThisServer in $AdsiServer) { if (!($KnownServers[$ThisServer])) { $AdsiProvider = Find-AdsiProvider -AdsiServer $ThisServer $WellKnownSIDs = Get-WellKnownSid -CimServerName $ThisServer $KnownServers[$ThisServer] = [pscustomobject]@{ AdsiProvider = $AdsiProvider WellKnownSIDs = $WellKnownSIDs } } $KnownServers[$ThisServer] } } } function Get-CurrentDomain { <# .SYNOPSIS Use ADSI to get the current domain .DESCRIPTION Works only on domain-joined systems, otherwise returns nothing .INPUTS None. Pipeline input is not accepted. .OUTPUTS [System.DirectoryServices.DirectoryEntry] The current domain .EXAMPLE Get-CurrentDomain Get the domain of the current computer #> [OutputType([System.DirectoryServices.DirectoryEntry])] $Obj = [adsi]::new() try { $null = $Obj.RefreshCache('objectSid') } catch { return } return $Obj } function Get-DirectoryEntry { <# .SYNOPSIS Use Active Directory Service Interfaces to retrieve an object from a directory .DESCRIPTION Retrieve a directory entry using either the WinNT or LDAP provider for ADSI .INPUTS None. Pipeline input is not accepted. .OUTPUTS [System.DirectoryServices.DirectoryEntry] where possible [PSCustomObject] for security principals with no directory entry .EXAMPLE Get-DirectoryEntry distinguishedName : {DC=ad,DC=contoso,DC=com} Path : LDAP://DC=ad,DC=contoso,DC=com As the current user on a domain-joined computer, bind to the current domain and retrieve the DirectoryEntry for the root of the domain .EXAMPLE Get-DirectoryEntry distinguishedName : Path : WinNT://ComputerName As the current user on a workgroup computer, bind to the local system and retrieve the DirectoryEntry for the root of the directory #> [OutputType([System.DirectoryServices.DirectoryEntry], [PSCustomObject])] [CmdletBinding()] param ( <# Path to the directory object to retrieve Defaults to the root of the current domain #> [string]$DirectoryPath = (([System.DirectoryServices.DirectorySearcher]::new()).SearchRoot.Path), <# Credentials to use to bind to the directory Defaults to the credentials of the current user #> [pscredential]$Credential, # Properties of the target object to retrieve [string[]]$PropertiesToLoad, <# Hashtable containing cached directory entries so they don't have to be retrieved from the directory again Uses a thread-safe hashtable by default #> [hashtable]$DirectoryEntryCache = ([hashtable]::Synchronized(@{})) ) $DirectoryEntry = $null if ($null -eq $DirectoryEntryCache[$DirectoryPath]) { switch -regex ($DirectoryPath) { <# The WinNT provider only throws an error if you try to retrieve certain accounts/identities We will create own dummy objects instead of performing the query #> '^WinNT:\/\/.*\/CREATOR OWNER$' { $DirectoryEntry = New-FakeDirectoryEntry -DirectoryPath $DirectoryPath } '^WinNT:\/\/.*\/SYSTEM$' { $DirectoryEntry = New-FakeDirectoryEntry -DirectoryPath $DirectoryPath } '^WinNT:\/\/.*\/INTERACTIVE$' { $DirectoryEntry = New-FakeDirectoryEntry -DirectoryPath $DirectoryPath } '^WinNT:\/\/.*\/Authenticated Users$' { $DirectoryEntry = New-FakeDirectoryEntry -DirectoryPath $DirectoryPath } '^WinNT:\/\/.*\/TrustedInstaller$' { $DirectoryEntry = New-FakeDirectoryEntry -DirectoryPath $DirectoryPath } # Workgroup computers do not return a DirectoryEntry with a SearchRoot Path so this ends up being an empty string '^$' { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tGet-DirectoryEntry`t$(hostname) does not appear to be domain-joined since the SearchRoot Path is empty. Defaulting to WinNT provider for localhost instead." $Workgroup = (Get-CimInstance -ClassName Win32_ComputerSystem).Workgroup $DirectoryPath = "WinNT://$Workgroup/$(hostname)" Write-Debug " $(Get-Date -Format s)`t$(hostname)`tGet-DirectoryEntry`t[System.DirectoryServices.DirectoryEntry]::new('$DirectoryPath')" if ($Credential) { $DirectoryEntry = [System.DirectoryServices.DirectoryEntry]::new($DirectoryPath, $($Credential.UserName), $($Credential.GetNetworkCredential().password)) } else { $DirectoryEntry = [System.DirectoryServices.DirectoryEntry]::new($DirectoryPath) } $SampleUser = $DirectoryEntry.PSBase.Children | Where-Object -FilterScript { $_.schemaclassname -eq 'user' } | Select-Object -First 1 | Add-SidInfo $DirectoryEntry | Add-Member -MemberType NoteProperty -Name 'Domain' -Value $SampleUser.Domain -Force } # Otherwise the DirectoryPath is an LDAP path default { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tGet-DirectoryEntry`t[System.DirectoryServices.DirectoryEntry]::new('$DirectoryPath')" if ($Credential) { $DirectoryEntry = [System.DirectoryServices.DirectoryEntry]::new($DirectoryPath, $($Credential.UserName), $($Credential.GetNetworkCredential().password)) } else { $DirectoryEntry = [System.DirectoryServices.DirectoryEntry]::new($DirectoryPath) } } } $DirectoryEntryCache[$DirectoryPath] = $DirectoryEntry } else { #Write-Debug " $(Get-Date -Format s)`t$(hostname)`tGet-DirectoryEntry`tDirectoryEntryCache hit for '$DirectoryPath'" $DirectoryEntry = $DirectoryEntryCache[$DirectoryPath] } if ($PropertiesToLoad) { try { # If the $DirectoryPath was invalid, this line will return an error $null = $DirectoryEntry.RefreshCache($PropertiesToLoad) } catch { Write-Warning "$(Get-Date -Format s)`t$(hostname)`tGet-DirectoryEntry`t'$DirectoryPath' could not be retrieved." # Ensure that the error message appears on 1 line # Use .Trim() to remove leading and trailing whitespace # Use -replace to remove an errant line break in the following specific error I encountered: The following exception occurred while retrieving member "RefreshCache": "The group name could not be found.`r`n" Write-Warning "$(Get-Date -Format s)`t$(hostname)`tGet-DirectoryEntry`t'$($_.Exception.Message.Trim() -replace '\s"',' "')" return } } return $DirectoryEntry } function Get-TrustedDomainSidNameMap { <# .SYNOPSIS Returns a dictionary of trusted domains by the current computer .DESCRIPTION Works only on domain-joined systems Use nltest to get the domain trust relationships for the domain of the current computer Use ADSI's LDAP provider to get each trusted domain's DNS name, NETBIOS name, and SID For each trusted domain the key is the domain's SID, or its NETBIOS name if the -KeyByNetbios switch parameter was used For each trusted domain the value contains the details retrieved with ADSI .INPUTS None. Pipeline input is not accepted. .OUTPUTS [System.Collections.Hashtable] The current domain trust relationships .EXAMPLE Get-TrustedDomainSidNameMap Get the trusted domains of the current computer #> [OutputType([System.Collections.Hashtable])] param ( # Key the dictionary by the domain NetBIOS names instead of SIDs [Switch]$KeyByNetbios, <# Hashtable containing cached directory entries so they don't have to be retrieved from the directory again Uses a thread-safe hashtable by default #> [hashtable]$DirectoryEntryCache = ([hashtable]::Synchronized(@{})) ) $Map = @{} # Redirect the error stream to null $nltestresults = & nltest /domain_trusts 2> $null $NlTestRegEx = '[\d]*: .*' $TrustRelationships = $nltestresults -match $NlTestRegEx foreach ($TrustRelationship in $TrustRelationships) { $RegEx = '(?<index>[\d]*): (?<netbios>\S*) (?<dns>\S*).*' if ($TrustRelationship -match $RegEx) { $DomainDnsName = $Matches.dns $DomainNetbios = $Matches.netbios } $DomainDirectoryEntry = Get-DirectoryEntry -DirectoryPath "LDAP://$DomainDnsName" -DirectoryEntryCache $DirectoryEntryCache try { $null = $DomainDirectoryEntry.RefreshCache('objectSid') } catch { Write-Warning "$(Get-Date -Format s)`t$(hostname)`tGet-TrustedDomainSidNameMap`tLDAP Domain: '$DomainDnsName' - $($_.Exception.Message)" continue } try { $DomainSid = [System.Security.Principal.SecurityIdentifier]::new([byte[]]$DomainDirectoryEntry.Properties["objectSid"].Value, 0).ToString() } catch { Write-Warning "$(Get-Date -Format s)`t$(hostname)`tGet-TrustedDomainSidNameMap`tLDAP Domain: '$DomainDnsName' has an invalid SID - $($_.Exception.Message)" continue } $DistinguishedName = ConvertTo-DistinguishedName -Domain $DomainNetbios if ($KeyByNetbios -eq $true) { $Map[$DomainNetbios] = [pscustomobject]@{ Dns = $DomainDnsName Netbios = $DomainNetbios Sid = $DomainSid DistinguishedName = $DistinguishedName } } else { $Map[$DomainSid] = [pscustomobject]@{ Dns = $DomainDnsName Netbios = $DomainNetbios Sid = $DomainSid DistinguishedName = $DistinguishedName } } } # Add the WinNT domain of the local computer as well $LocalAccountSID = Get-CimInstance -Query "SELECT SID FROM Win32_UserAccount WHERE LocalAccount = 'True'" | Select-Object -First 1 -ExpandProperty SID $DomainSid = $LocalAccountSID.Substring(0, $LocalAccountSID.LastIndexOf("-")) $DomainNetBios = hostname $DomainDnsName = "$DomainNetbios.$((Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters').'NV Domain')" if ($KeyByNetbios -eq $true) { $Map[$DomainNetbios] = [pscustomobject]@{ Dns = $DomainDnsName Netbios = $DomainNetbios Sid = $DomainSid DistinguishedName = $null } } else { $Map[$DomainSid] = [pscustomobject]@{ Dns = $DomainDnsName Netbios = $DomainNetbios Sid = $DomainSid DistinguishedName = $null } } return $Map } function Get-WellKnownSid { <# .SYNOPSIS Use CIM to get well-known SIDs .DESCRIPTION Use WinRM to query the CIM namespace root/cimv2 for instances of the Win32_Account class .INPUTS [System.String]$CimServerName .OUTPUTS [Microsoft.Management.Infrastructure.CimInstance] for each instance of the Win32_Account class in the root/cimv2 namespace .EXAMPLE Get-WellKnownSid Get the well-known SIDs on the current computer .EXAMPLE Get-WellKnownSid -CimServerName 'server123' Get the well-known SIDs on the remote computer 'server123' #> [OutputType([Microsoft.Management.Infrastructure.CimInstance])] param ( [Parameter(ValueFromPipeline)] [string[]]$CimServerName ) process { ForEach ($ThisServer in $CimServerName) { if ($ThisServer -eq (hostname) -or $ThisServer -eq 'localhost' -or $ThisServer -eq '127.0.0.1') { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tGet-WellKnownSid`tNew-CimSession" $CimSession = New-CimSession } else { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tGet-WellKnownSid`tNew-CimSession -ComputerName '$ThisServer'" $CimSession = New-CimSession -ComputerName $ThisServer } $Results = @{} Get-CimInstance -ClassName Win32_Account -CimSession $CimSession | ForEach-Object { $Results[$_.Name] = $_ } $Results Remove-CimSession -CimSession $CimSession } } } function Get-WinNTGroupMember { <# .SYNOPSIS Get members of a group from the WinNT provider .DESCRIPTION Get members of a group from the WinNT provider Convert them from COM objects into usable DirectoryEntry objects .INPUTS [System.DirectoryServices.DirectoryEntry]$DirectoryEntry .OUTPUTS [System.DirectoryServices.DirectoryEntry] for each group member .EXAMPLE [System.DirectoryServices.DirectoryEntry]::new('WinNT://localhost/Administrators') | Get-WinNTGroupMember Get members of the local Administrators group #> [OutputType([System.DirectoryServices.DirectoryEntry])] param ( # DirectoryEntry [System.DirectoryServices.DirectoryEntry] of the WinNT group whose members to get [Parameter(ValueFromPipeline)] $DirectoryEntry, <# Hashtable containing cached directory entries so they don't have to be retrieved from the directory again Uses a thread-safe hashtable by default #> [hashtable]$DirectoryEntryCache = ([hashtable]::Synchronized(@{})), # Properties of the group members to find in the directory [string[]]$PropertiesToLoad, # Hashtable of domain DNs $KnownDomains = (Get-TrustedDomainSidNameMap -DirectoryEntryCache $DirectoryEntryCache -KeyByNetbios) ) process { ForEach ($ThisDirEntry in $DirectoryEntry) { $SourceDomain = $ThisDirEntry.Path | Split-Path -Parent | Split-Path -Leaf # Retrieve the members of local groups if ($null -ne $ThisDirEntry.Properties['groupType'] -or $ThisDirEntry.schemaclassname -contains 'Group') { # Assembly: System.DirectoryServices.dll # Namespace: System.DirectoryServices # DirectoryEntry.Invoke(String, Object[]) Method # Calls a method on the native Active Directory Domain Services object # https://docs.microsoft.com/en-us/dotnet/api/system.directoryservices.directoryentry.invoke?view=dotnet-plat-ext-6.0 # I am using it to call the IADsGroup::Members method # The IADsGroup programming interface is part of the iads.h header # The iads.h header is part of the ADSI component of the Win32 API # The IADsGroup::Members method retrieves a collection of the immediate members of the group. # The collection does not include the members of other groups that are nested within the group. # The default implementation of this method uses LsaLookupSids to query name information for the group members. # LsaLookupSids has a maximum limitation of 20480 SIDs it can convert, therefore that limitation also applies to this method. # Returns a pointer to an IADsMembers interface pointer that receives the collection of group members. The caller must release this interface when it is no longer required. # https://docs.microsoft.com/en-us/windows/win32/api/iads/nf-iads-iadsgroup-members # The IADsMembers::Members method would use the same provider but I have chosen not to implement that here # Recursion through nested groups can be handled outside of Get-WinNTGroupMember for now # Maybe that could be a feature in the future # https://docs.microsoft.com/en-us/windows/win32/adsi/adsi-object-model-for-winnt-providers?redirectedfrom=MSDN $DirectoryMembers = & { $ThisDirEntry.Invoke('Members') } 2>$null Write-Debug " $(Get-Date -Format s)`t$(hostname)`tGet-WinNTGroupMember`t'$($ThisDirEntry.Path)' has $(($DirectoryMembers | Measure-Object).Count) members" ForEach ($DirectoryMember in $DirectoryMembers) { # The IADsGroup::Members method returns ComObjects # But proper .Net objects are much easier to work with # So we will convert the ComObjects into DirectoryEntry objects $DirectoryPath = Invoke-ComObject -ComObject $DirectoryMember -Property 'ADsPath' $MemberDomainDn = $null if ($DirectoryPath -match 'WinNT:\/\/(?<Domain>[^\/]*)\/(?<Acct>.*$)') { $MemberName = $Matches.Acct $MemberDomainNetbios = $Matches.Domain if ($KnownDomains[$MemberDomainNetbios] -and $MemberDomainNetbios -ne $SourceDomain) { $MemberDomainDn = $KnownDomains[$MemberDomainNetbios].DistinguishedName } if ($DirectoryPath -match 'WinNT:\/\/(?<Domain>[^\/]*)\/(?<Middle>[^\/]*)\/(?<Acct>.*$)') { if ($Matches.Middle -eq ($ThisDirEntry.Path | Split-Path -Parent | Split-Path -Leaf)) { $MemberDomainDn = $null } } } $MemberParams = @{ DirectoryEntryCache = $DirectoryEntryCache DirectoryPath = $DirectoryPath PropertiesToLoad = $PropertiesToLoad } if ($MemberDomainDn) { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tGet-WinNTGroupMember`t'$MemberName' is a domain security principal" $MemberParams['DirectoryPath'] = "LDAP://$MemberDomainDn" $MemberParams['Filter'] = "(samaccountname=$MemberName)" $MemberDirectoryEntry = Search-Directory @MemberParams } else { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tGet-WinNTGroupMember`t'$DirectoryPath' is a local security principal" $MemberDirectoryEntry = Get-DirectoryEntry @MemberParams } $MemberDirectoryEntry | Expand-WinNTGroupMember -DirectoryEntryCache $DirectoryEntryCache } } else { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tGet-WinNTGroupMember`t'$($ThisDirEntry.Path)' is not a group" } } } } function Invoke-ComObject { <# .SYNOPSIS Invoke a member method of a ComObject [__ComObject] .DESCRIPTION Use the InvokeMember method to invoke the InvokeMethod or GetProperty or SetProperty methods By default, invokes the GetProperty method for the specified Property If the Value parameter is specified, invokes the SetProperty method for the specified Property If the Method switch is specified, invokes the InvokeMethod method .INPUTS None. Pipeline input is not accepted. .OUTPUTS The output of the invoked method is returned directly .EXAMPLE $ComObject = [System.DirectoryServices.DirectoryEntry]::new('WinNT://localhost/Administrators').Invoke('Members') | Select -First 1 Invoke-ComObject -ComObject $ComObject -Property AdsPath Get the first member of the local Administrators group on the current computer Then use Invoke-ComObject to invoke the GetProperty method and return the value of the AdsPath property #> param ( # The ComObject whose member method to invoke [Parameter(Mandatory)] $ComObject, # The property to use with the invoked method [Parameter(Mandatory)] [String]$Property, # The value to set with the SetProperty method, or the name of the method to run with the InvokeMethod method $Value, # Use the InvokeMethod method of the ComObject [Switch]$Method ) <# # Don't remember what this is for If ($ComObject -IsNot "__ComObject") { If (!$ComInvoke) { $Global:ComInvoke = @{} } If (!$ComInvoke.$ComObject) { $ComInvoke.$ComObject = New-Object -ComObject $ComObject } $ComObject = $ComInvoke.$ComObject } #> If ($Method) { $Invoke = "InvokeMethod" } ElseIf ($MyInvocation.BoundParameters.ContainsKey("Value")) { $Invoke = "SetProperty" } Else { $Invoke = "GetProperty" } [__ComObject].InvokeMember($Property, $Invoke, $Null, $ComObject, $Value) } function New-FakeDirectoryEntry { <# .SYNOPSIS Returns a PSCustomObject in place of a DirectoryEntry for certain WinNT security principals that do not have objects in the directory .DESCRIPTION The WinNT provider only throws an error if you try to retrieve certain accounts/identities We will create dummy objects instead of performing the query .INPUTS None. Pipeline input is not accepted. .OUTPUTS [System.Management.Automation.PSCustomObject] .EXAMPLE ---------- EXAMPLE 1 ---------- New-FakeDirectoryEntry -DirectoryPath 'WinNT://WORKGROUP/Computer/CREATOR OWNER' Create a fake DirectoryEntry to represent the CREATOR OWNER special security principal #> [OutputType([System.Management.Automation.PSCustomObject])] param ( <# Path to the directory object to retrieve Defaults to the root of the current domain (but don't use it for that, just do this instead: [System.DirectoryServices.DirectorySearcher]::new()) #> [string]$DirectoryPath ) $DirectoryEntry = $null $Properties = @{ Name = ($DirectoryPath -split '\/') | Select-Object -Last 1 Parent = $DirectoryPath | Split-Path -Parent Path = $DirectoryPath SchemaEntry = [System.DirectoryServices.DirectoryEntry] } switch -regex ($DirectoryPath) { 'CREATOR OWNER$' { $Properties['objectSid'] = 'S-1-3-0' | ConvertTo-SidByteArray $Properties['Description'] = 'A SID to be replaced by the SID of the user who creates a new object. This SID is used in inheritable ACEs.' $Properties['Properties'] = @{ Name = $Properties['Name'] Description = $Description objectSid = $SidByteAray } $Properties['SchemaClassName'] = 'User' } 'SYSTEM$' { $Properties['objectSid'] = 'S-1-5-18' | ConvertTo-SidByteArray $Properties['Description'] = 'By default, the SYSTEM account is granted Full Control permissions to all files on an NTFS volume' $Properties['Properties'] = @{ Name = $Properties['Name'] Description = $Description objectSid = $SidByteAray } $Properties['SchemaClassName'] = 'User' } 'INTERACTIVE$' { $Properties['objectSid'] = 'S-1-5-4' | ConvertTo-SidByteArray $Properties['Description'] = 'Users who log on for interactive operation. This is a group identifier added to the token of a process when it was logged on interactively.' $Properties['Properties'] = @{ Name = $Properties['Name'] Description = $Description objectSid = $SidByteAray } $Properties['SchemaClassName'] = 'Group' } 'Authenticated Users$' { $Properties['objectSid'] = 'S-1-5-11' | ConvertTo-SidByteArray $Properties['Description'] = 'Any user who accesses the system through a sign-in process has the Authenticated Users identity.' $Properties['Properties'] = @{ Name = $Properties['Name'] Description = $Description objectSid = $SidByteAray } $Properties['SchemaClassName'] = 'Group' } 'TrustedInstaller$' { $Properties['objectSid'] = 'S-1-5-11' | ConvertTo-SidByteArray $Properties['Description'] = 'Most of the operating system files are owned by the TrustedInstaller security identifier (SID)' $Properties['Properties'] = @{ Name = $Properties['Name'] Description = $Description objectSid = $SidByteAray } $Properties['SchemaClassName'] = 'User' } } $DirectoryEntry = [pscustomobject]::new($Properties) $DirectoryEntry | Add-Member -MemberType ScriptMethod -Name RefreshCache -Force -Value {} return $DirectoryEntry } function Resolve-Ace { <# .SYNOPSIS Use ADSI to lookup info about IdentityReferences from Authorization Rule Collections that came from Discretionary Access Control Lists .DESCRIPTION Based on the IdentityReference proprety of each Access Control Entry: Resolve SID to NT account name and vise-versa Resolve well-known SIDs Resolve generic defaults like 'NT AUTHORITY' and 'BUILTIN' to the applicable computer or domain name Add these properties (IdentityReferenceSID,IdentityReferenceName,IdentityReferenceResolved) to the object and return it .INPUTS [System.Security.AccessControl.AuthorizationRuleCollection]$InputObject .OUTPUTS [PSCustomObject] Original object plus IdentityReferenceSID,IdentityReferenceName,IdentityReferenceResolved, and AdsiProvider properties .EXAMPLE Get-Acl | Expand-Acl | Resolve-Ace Use Get-Acl from the Microsoft.PowerShell.Security module as the source of the access list This works in either Windows Powershell or in Powershell Get-Acl does not support long paths (>256 characters) That was why I originally used the .Net Framework method .EXAMPLE Get-FolderAce -LiteralPath C:\Test -IncludeInherited | Resolve-Ace .EXAMPLE [System.String]$FolderPath = 'C:\Test' [System.IO.DirectoryInfo]$DirectoryInfo = Get-Item -LiteralPath $FolderPath $Sections = [System.Security.AccessControl.AccessControlSections]::Access -bor [System.Security.AccessControl.AccessControlSections]::Owner $FileSecurity = [System.Security.AccessControl.FileSecurity]::new($DirectoryInfo,$Sections) $IncludeExplicitRules = $true $IncludeInheritedRules = $true $AccountType = [System.Security.Principal.SecurityIdentifier] $FileSecurity.GetAccessRules($IncludeExplicitRules,$IncludeInheritedRules,$AccountType) | Resolve-Ace This uses .Net Core as the source of the access list It uses the GetAccessRules method on the [System.Security.AccessControl.FileSecurity] class The targetType parameter of the method is used to specify that the accounts in the ACL are returned as SIDs .EXAMPLE [System.String]$FolderPath = 'C:\Test' [System.IO.DirectoryInfo]$DirectoryInfo = Get-Item -LiteralPath $FolderPath $Sections = [System.Security.AccessControl.AccessControlSections]::Access -bor [System.Security.AccessControl.AccessControlSections]::Owner -bor [System.Security.AccessControl.AccessControlSections]::Group $DirectorySecurity = [System.Security.AccessControl.DirectorySecurity]::new($DirectoryInfo,$Sections) $IncludeExplicitRules = $true $IncludeInheritedRules = $true $AccountType = [System.Security.Principal.NTAccount] $FileSecurity.GetAccessRules($IncludeExplicitRules,$IncludeInheritedRules,$AccountType) | Resolve-Ace This uses .Net Core as the source of the access list It uses the GetAccessRules method on the [System.Security.AccessControl.FileSecurity] class The targetType parameter of the method is used to specify that the accounts in the ACL are returned as NT account names (DOMAIN\User) .EXAMPLE [System.String]$FolderPath = 'C:\Test' [System.IO.DirectoryInfo]$DirectoryInfo = Get-Item -LiteralPath $FolderPath [System.Security.AccessControl.DirectorySecurity]$DirectorySecurity = $DirectoryInfo.GetAccessControl('Access') [System.Security.AccessControl.AuthorizationRuleCollection]$AuthRules = $DirectorySecurity.Access $AuthRules | Resolve-Ace Use the .Net Framework (or legacy .Net Core up to 2.2) as the source of the access list Only works in Windows PowerShell Those versions of .Net had a GetAccessControl method on the [System.IO.DirectoryInfo] class This method is removed in modern versions of .Net Core .EXAMPLE [System.String]$FolderPath = 'C:\Test' [System.IO.DirectoryInfo]$DirectoryInfo = Get-Item -LiteralPath $FolderPath $Sections = [System.Security.AccessControl.AccessControlSections]::Access -bor [System.Security.AccessControl.AccessControlSections]::Owner $FileSecurity = [System.IO.FileSystemAclExtensions]::GetAccessControl($DirectoryInfo,$Sections) The [System.IO.FileSystemAclExtensions] class is a Windows-specific implementation It provides no known benefit over the cross-platform equivalent [System.Security.AccessControl.FileSecurity] .NOTES Dependencies: Get-DirectoryEntry Add-SidInfo Get-TrustedDomainSidNameMap Find-AdsiProvider if ($FolderPath.Length -gt 255) { $FolderPath = "\\?\$FolderPath" } #> [OutputType([PSCustomObject])] param ( # Authorization Rule Collection of Access Control Entries from Discretionary Access Control Lists [Parameter( ValueFromPipeline )] [PSObject[]]$InputObject, # Dictionary to cache known servers to avoid redundant lookups # Defaults to an empty thread-safe hashtable [hashtable]$KnownServers = [hashtable]::Synchronized(@{}) ) process { $ACEPropertyNames = (Get-Member -InputObject $InputObject[0] -MemberType Property, CodeProperty, ScriptProperty, NoteProperty).Name ForEach ($ThisACE in $InputObject) { # Remove the PsProvider prefix from the path string if (-not [string]::IsNullOrEmpty($ThisACE.SourceAccessList.Path)) { $LiteralPath = $ThisACE.SourceAccessList.Path -replace [regex]::escape("$($ThisACE.SourceAccessList.PsProvider)::"), '' } else { $LiteralPath = $LiteralPath -replace [regex]::escape("$($ThisACE.SourceAccessList.PsProvider)::"), '' } $ThisServer = Find-ServerNameInPath -LiteralPath $LiteralPath $AdsiServer = Get-AdsiServer -AdsiServer $ThisServer -KnownServers $KnownServers $ResolvedIdentityReference = Resolve-IdentityReference -IdentityReference $ThisACE.IdentityReference -ServerName $ThisServer -AdsiServer $AdsiServer $FullyResolved = $ResolvedIdentityReference.UnresolvedIdentityReference -replace 'NT AUTHORITY', $ThisServer -replace 'NT SERVICE', $ThisServer -replace 'BUILTIN', $ThisServer $ObjectProperties = @{ AdsiProvider = $AdsiServer.AdsiProvider AdsiServer = $ThisServer IdentityReferenceSID = $ResolvedIdentityReference.SIDString IdentityReferenceName = $ResolvedIdentityReference.UnresolvedIdentityReference IdentityReferenceResolved = $FullyResolved #Path = $LiteralPath #PathProvider = $PsProvider #PathAreAccessRulesProtected = $ThisACE.SourceAccessList.AreAccessRulesProtected } ForEach ($ThisProperty in $ACEPropertyNames) { $ObjectProperties[$ThisProperty] = $ThisACE.$ThisProperty } [PSCustomObject]$ObjectProperties } } } function Resolve-IdentityReference { <# .SYNOPSIS Use ADSI to lookup info about IdentityReferences from Access Control Entries that came from Discretionary Access Control Lists .DESCRIPTION Based on the IdentityReference proprety of each Access Control Entry: Resolve SID to NT account name and vise-versa Resolve well-known SIDs Resolve generic defaults like 'NT AUTHORITY' and 'BUILTIN' to the applicable computer or domain name .INPUTS None. Pipeline input is not accepted. .OUTPUTS [PSCustomObject] with UnresolvedIdentityReference and SIDString properties (each strings) .EXAMPLE Resolve-IdentityReference -IdentityReference 'BUILTIN\Administrator' -ServerName 'localhost' -AdsiServer (Get-AdsiServer 'localhost') Get information about the local Administrator account #> [OutputType([PSCustomObject])] param ( # IdentityReference from an Access Control Entry # Expecting either a SID (S-1-5-18) or an NT account name (CONTOSO\User) [string]$IdentityReference, # Name of the directory server to use to resolve the IdentityReference [string]$ServerName, # Object from Get-AdsiServer representing the directory server and its attributes [PSObject]$AdsiServer ) $ThisHostName = hostname if ($IdentityReference -like 'S-1-*') { # The IdentityReference is a SID Write-Debug " $(Get-Date -Format s)`t$ThisHostName`tResolve-IdentityReference`t[System.Security.Principal.SecurityIdentifier]::new('$IdentityReference')" $SecurityIdentifier = [System.Security.Principal.SecurityIdentifier]::new($IdentityReference) # This .Net method makes it impossible to redirect the error stream directly # Wrapping it in a scriptblock (which is then executed with &) fixes the problem # I don't understand exactly why Write-Debug " $(Get-Date -Format s)`t$ThisHostName`tResolve-IdentityReference`t[System.Security.Principal.SecurityIdentifier]::new('$IdentityReference').Translate([System.Security.Principal.NTAccount])" $UnresolvedIdentityReference = & { $SecurityIdentifier.Translate([System.Security.Principal.NTAccount]).Value } 2>$null $SIDString = $IdentityReference } else { # The IdentityReference is an NTAccount $UnresolvedIdentityReference = $IdentityReference # Resolve NTAccount to SID $Name = ($UnresolvedIdentityReference -split '\\')[1] # Well-Known SIDs cannot be translated with the Translate method so instead we will use CIM $SIDString = $AdsiServer.WellKnownSIDs[$Name].SID if (!($SIDString)) { Write-Debug " $(Get-Date -Format s)`t$ThisHostName`tResolve-IdentityReference`t[System.Security.Principal.NTAccount]::new('$ServerName','$IdentityReference')" $NTAccount = [System.Security.Principal.NTAccount]::new($ServerName, $IdentityReference) Write-Debug " $(Get-Date -Format s)`t$ThisHostName`tResolve-IdentityReference`t[System.Security.Principal.NTAccount]::new('$ServerName','$IdentityReference').Translate([System.Security.Principal.SecurityIdentifier])" $SIDString = & { $NTAccount.Translate([System.Security.Principal.SecurityIdentifier]) } 2>$null if (!($SIDString)) { # Some built-in groups such as BUILTIN\Users and BUILTIN\Administrators are not in the CIM class or translatable with the Translate method if ($UnresolvedIdentityReference -like "NT SERVICE\*") { # Some of them are services (yes services can have SIDs) if ($ServerName -eq $ThisHostName) { Write-Debug " $(Get-Date -Format s)`t$ThisHostName`tResolve-IdentityReference`tsc.exe showsid $Name" [string[]]$ScResult = & sc.exe showsid $Name } else { Write-Debug " $(Get-Date -Format s)`t$ThisHostName`tResolve-IdentityReference`tInvoke-Command -ComputerName $ServerName -ScriptBlock { & sc.exe showsid `$args[0] } -ArgumentList $Name" [string[]]$ScResult = Invoke-Command -ComputerName $ServerName -ScriptBlock { & sc.exe showsid $args[0] } -ArgumentList $Name } $ScResultProps = @{} $ScResult | ForEach-Object { $Prop, $Value = ($_ -split ':').Trim() $ScResultProps[$Prop] = $Value } $SIDString = $ScResultProps['SERVICE SID'] } else { # Otherwise they may have real DirectoryEntry objects $DirectoryPath = "$($AdsiServer.AdsiProvider)`://$ServerName/$Name" $SIDString = (Get-DirectoryEntry -DirectoryPath $DirectoryPath | Add-SidInfo).SidString } } } } [PSCustomObject]@{ UnresolvedIdentityReference = $UnresolvedIdentityReference SIDString = $SIDString } } function Search-Directory { <# .SYNOPSIS Use Active Directory Service Interfaces to search an LDAP directory .DESCRIPTION Find directory entries using the LDAP provider for ADSI (the WinNT provider does not support searching) Provides a wrapper around the [System.DirectoryServices.DirectorySearcher] class .INPUTS None. Pipeline input is not accepted. .OUTPUTS [System.DirectoryServices.DirectoryEntry] .EXAMPLE Search-Directory -Filter '' As the current user on a domain-joined computer, bind to the current domain and search for all directory entries matching the LDAP filter #> param ( <# Path to the directory object to retrieve Defaults to the root of the current domain #> [string]$DirectoryPath = (([adsisearcher]'').SearchRoot.Path), # Filter for the LDAP search [string]$Filter, # Number of records per page of results [int]$PageSize = 1000, # Additional properties to return [string[]]$PropertiesToLoad, # Credentials to use [pscredential]$Credential, # Scope of the search [string]$SearchScope = 'subtree', <# Hashtable containing cached directory entries so they don't have to be retrieved from the directory again Uses a thread-safe hashtable by default #> [hashtable]$DirectoryEntryCache = ([hashtable]::Synchronized(@{})) ) $DirectoryEntryParameters = @{ DirectoryEntryCache = $DirectoryEntryCache } if ($Credential) { $DirectoryEntryParameters['Credential'] = $Credential } if (($null -eq $DirectoryPath -or '' -eq $DirectoryPath)) { $Workgroup = (Get-CimInstance -ClassName Win32_ComputerSystem).Workgroup $DirectoryPath = "WinNT://$Workgroup/$(hostname)" } $DirectoryEntryParameters['DirectoryPath'] = $DirectoryPath $DirectoryEntry = Get-DirectoryEntry @DirectoryEntryParameters $DirectorySearcher = [System.DirectoryServices.DirectorySearcher]::new($DirectoryEntry) if ($Filter) { $DirectorySearcher.Filter = $Filter } $DirectorySearcher.PageSize = $PageSize $DirectorySearcher.SearchScope = $SearchScope ForEach ($Property in $PropertiesToLoad) { $null = $DirectorySearcher.PropertiesToLoad.Add($Property) } $SearchResultCollection = $DirectorySearcher.FindAll() # TODO: Fix this. Problems in integration testing trying to use the objects later if I dispose them here now. # Error: Cannot access a disposed object. #$null = $DirectorySearcher.Dispose() #$null = $DirectoryEntry.Dispose() $Output = [System.DirectoryServices.SearchResult[]]::new($SearchResultCollection.Count) $SearchResultCollection.CopyTo($Output, 0) #$null = $SearchResultCollection.Dispose() return $Output } <# # Add any custom C# classes as usable (exported) types $CSharpFiles = Get-ChildItem -Path "$PSScriptRoot\*.cs" ForEach ($ThisFile in $CSharpFiles) { Add-Type -Path $ThisFile.FullName -ErrorAction Stop } #> # Definition of Module 'SimplePrtg' is below function Format-PrtgXmlResult { <# .SYNOPSIS Generate an XML result for a single channel to include in the result for a PRTG custom XML sensor .DESCRIPTION Generate a <result>...</result> XML channel for a PRTG custom XML sensor .INPUTS [System.String]$Channel .OUTPUTS [System.String] A single XML channel to include in the output for a PRTG XML sensor .EXAMPLE New-PrtgXmlResult -Channel 'Channel123' -Value 'Value123' -CustomUnit 'Miles Per Hour' <result> <channel>Channel123</channel> <value>Value123</value> <unit>Custom</unit> <customUnit>Miles Per Hour</customUnit> <showchart>0</showchart> </result> Generate XML output for a PRTG sensor that will put it in an OK state #> param ( # PRTG sensor channel of the result [parameter(Mandatory)] [string]$Channel, # Value to return [parameter(Mandatory)] [string]$Value, # Reccomend leaving this as 'Custom' but see PRTG docs for other options [string]$Unit = 'Custom', # Custom unit label to apply to the value [string]$CustomUnit, # Show the channel on charts in PRTG [int]$ShowChart = 0, # If the value goes above this the channel will be in an alarm state in PRTG [string]$MaxError, # If the value goes below this the channel will be in an alarm state in PRTG [string]$MinError, # If the value goes above this the channel will be in a warning state in PRTG [string]$MaxWarn, # If the value goes below this the channel will be in a warning state in PRTG [string]$MinWarn, # Force the channel into a warning state in PRTG [switch]$Warning ) $Xml = [System.Collections.Generic.List[string]]::new() $null = $Xml.Add('<result>') $null = $Xml.Add(" <channel>$Channel</channel>") $null = $Xml.Add(" <value>$Value</value>") $null = $Xml.Add(" <unit>$Unit</unit>") $null = $Xml.Add(" <showchart>$ShowChart</showchart>") if ($CustomUnit) { $null = $Xml.Add(" <customUnit>$CustomUnit</customUnit>") } if ($MaxError -or $MinError -or $MaxWarn -or $MinWarn) { $null = $Xml.Add(" <limitmode>1</limitmode>") if ($MaxError) { $null = $Xml.Add(" <limitmaxerror>$MaxError</limitmaxerror>") } if ($MinError) { $null = $Xml.Add(" <limitminerror>$MinError</limitminerror>") } if ($MaxWarn) { $null = $Xml.Add(" <limitmaxwarn>$MaxWarn</limitmaxwarn>") } if ($MinWarn) { $null = $Xml.Add(" <limitminwarn>$MinWarn</limitminwarn>") } } if ($Warning) { $null = $Xml.Add(' <Warning>1</Warning>') } else { $null = $Xml.Add(' <Warning>0</Warning>') } $null = $Xml.Add('</result>') $Xml } function Format-PrtgXmlSensorOutput { <# .SYNOPSIS Assemble the complete output for a PRTG XML sensor .DESCRIPTION Combine multiple channels into a single PRTG XML sensor result .INPUTS [System.String]$PrtgXmlResult .OUTPUTS [System.String] Complete XML output for a PRTG custom XML sensor .EXAMPLE @" <result> <channel>Channel123</channel> <value>Value123</value> <unit>Custom</unit> <customUnit>Miles Per Hour</customUnit> <showchart>$ShowChart</showchart> </result> @" | New-PrtgXmlSensorOutput Generate XML output for a PRTG sensor that will put it in an OK state .EXAMPLE @" <result> <channel>Channel123</channel> <value>Value123</value> <unit>Custom</unit> <customUnit>Miles Per Hour</customUnit> <showchart>0</showchart> </result> @" | New-PrtgXmlSensorOutput -IssueDetected Generate XML output for a PRTG sensor that will put it in an alarm state #> param ( # Valid XML for a PRTG result for a single channel # Can be created by Format-PrtgXmlResult [Parameter(ValueFromPipeline)] [string[]]$PrtgXmlResult, # Force the PRTG sensor into an alarm state [switch]$IssueDetected ) begin { $Strings = [System.Collections.Generic.List[string]]::new() $null = $Strings.add("<prtg>") } process { foreach ($XmlResult in $PrtgXmlResult) { $null = $Strings.add($XmlResult) } } end { if ($IssueDetected) { $null = $Strings.add("<text>Issue detected, see sensor channels for details</text>") } else { $null = $Strings.add("<text>OK</text>") } $null = $Strings.add("</prtg>") $Strings } } function Send-PrtgXmlSensorOutput { <# .SYNOPSIS Wrapper for Invoke-WebRequest to make it easy to push results to PRTG XML push sensors .DESCRIPTION Use HTTP post to post results to PRTG XML push sensors .INPUTS [System.String]$XmlOutput .OUTPUTS Passes through the output of Invoke-WebRequest .EXAMPLE New-PrtgXmlSensorOutput ... | Send-PrtgXmlSensorOutput -PrtgSensorProtocol 'https' -PrtgProbe 'server1' -PrtgSensorPort 443 -PrtgSensorToken 'e3edd633-3018-4d8a-91b6-d2635b42b85b' Post sensor output to PRTG push sensor e3edd633-3018-4d8a-91b6-d2635b42b85b on server1 using HTTPS on TCP port 443 #> param( # Valid XML for a PRTG custom XML sensor # Can be created by Format-PrtgXmlSensorOutput [string]$XmlOutput, # If all four of the PRTG parameters are specified, then the results will be XML-formatted and pushed to the specified PRTG probe for a push sensor [string]$PrtgProbe, # If all four of the PRTG parameters are specified, then the results will be XML-formatted and pushed to the specified PRTG probe for a push sensor [string]$PrtgSensorProtocol, # If all four of the PRTG parameters are specified, then the results will be XML-formatted and pushed to the specified PRTG probe for a push sensor [int]$PrtgSensorPort, # If all four of the PRTG parameters are specified, then the results will be XML-formatted and pushed to the specified PRTG probe for a push sensor [string]$PrtgSensorToken ) $ResultToPost = @{ Body = $XMLOutput ContentType = 'application/xml' Method = 'Post' Uri = "$PrtgSensorProtocol`://$PrtgProbe`:$PrtgSensorPort/$PrtgSensorToken" UseBasicParsing = $true } if ($PrtgSensorToken) { Write-Verbose "URI: $PrtgSensorProtocol`://$PrtgProbe`:$PrtgSensorPort/$PrtgSensorToken" Invoke-WebRequest @ResultToPost } } <# # Add any custom C# classes as usable (exported) types $CSharpFiles = Get-ChildItem -Path "$PSScriptRoot\*.cs" ForEach ($ThisFile in $CSharpFiles) { Add-Type -Path $ThisFile.FullName -ErrorAction Stop } #> # Definition of Module 'PsNtfs' is below function GetDirectories { param ( [Parameter(Mandatory)] [string]$TargetPath, [string]$SearchPattern = '*', [System.IO.SearchOption]$SearchOption = [System.IO.SearchOption]::AllDirectories ) Write-Debug " $(Get-Date -Format s)`t$(hostname)`tGetDirectories`t[System.IO.Directory]::GetDirectories('$TargetPath','$SearchPattern',[System.IO.SearchOption]::$SearchOption)" try { [System.IO.Directory]::GetDirectories($TargetPath, $SearchPattern, $SearchOption) } catch { Write-Warning "$(Get-Date -Format s)`t$(hostname)`tGetDirectories`t$($_.Exception.Message)" } } function Expand-AccountPermission { <# .SYNOPSIS Convert an object representing a security principal into a collection of objects respresenting the access control entries for that principal .DESCRIPTION Convert an object from Format-SecurityPrincipal (one object per principal, containing nested access entries) into flat objects (one per access entry per account) .INPUTS [pscustomobject]$AccountPermission .OUTPUTS [pscustomobject] One object per access control entry per account .EXAMPLE (Get-Acl).Access | Group-Object -Property IdentityReference | Expand-IdentityReference | Format-SecurityPrincipal | Expand-AccountPermission Incomplete example but it shows the chain of functions to generate the expected input for this #> param ( # Object that was output from Format-SecurityPrincipal $AccountPermission ) ForEach ($Account in $AccountPermission) { $PropertiesToExclude = @( 'NativeObject', 'NtfsAccessControlEntries', 'Group' ) $Props = @{} $AccountNoteProperties = $Account | Get-Member -MemberType Property, CodeProperty, ScriptProperty, NoteProperty | Where-Object -Property Name -NotIn $PropertiesToExclude ForEach ($ThisProperty in $AccountNoteProperties) { if ($null -eq $Props[$ThisProperty.Name]) { $Value = $Account.$($ThisProperty.Name) if ($null -ne $Value) { # We wrap this in an expression and use output redirection to supress this error: # The following exception occurred while retrieving member "GetType": "Not implemented" [string]$Type = & { $Value.GetType().FullName } 2>$null } else { [string]$Type = $null } switch ($Type) { 'System.DirectoryServices.PropertyCollection' { ForEach ($ThisAccountProperty in $Account.Properties.Keys) { $Props[$ThisAccountProperty] = ConvertFrom-PropertyValueCollectionToString -PropertyValueCollection $Account.Properties[$ThisAccountProperty] } $Props[$ThisProperty.Name] = "Converted to properties prefixed with AccountProperty" } 'System.DirectoryServices.PropertyValueCollection' { $Props[$ThisProperty.Name] = ConvertFrom-PropertyValueCollectionToString -PropertyValueCollection $Value } default { <# By default we will just let most types get cast as a string Includes but not limited to: $null (because GetType is not implemented) System.String System.Boolean System.Byte[] #> $Props[$ThisProperty.Name] = "$Value" } } } } ForEach ($ACE in $Account.NtfsAccessControlEntries) { $ACENoteProperties = $ACE | Get-Member -MemberType Property, CodeProperty, ScriptProperty, NoteProperty ForEach ($ThisProperty in $ACENoteProperties) { $Props["ACE$($ThisProperty.Name)"] = [string]$ACE.$($ThisProperty.Name) } [pscustomobject]$Props } } } function Expand-Acl { <# .SYNOPSIS Expand an Access Control List into its constituent Access Control Entries .DESCRIPTION Enumerate the members of the Access property of the $InputObject parameter (which is an AuthorizationRuleCollection or similar) Append the original ACL to each member as a SourceAccessList property Then return each member .INPUTS [PSObject]$InputObject Expected: [System.Security.AccessControl.DirectorySecurity]$InputObject from Get-Acl or [System.Security.AccessControl.FileSecurity]$InputObject from Get-Acl .OUTPUTS [PSCustomObject] .EXAMPLE Get-Acl | Expand-Acl Use Get-Acl from the Microsoft.PowerShell.Security module as the source of the access list This works in either Windows Powershell or in Powershell Get-Acl does not support long paths (>256 characters) That was why I originally used the .Net Framework method #> param ( # Access Control List whose Access Control Entries to return # Expects [System.Security.AccessControl.FileSecurity] objects from Get-Acl or otherwise # Expects [System.Security.AccessControl.DirectorySecurity] objects from Get-Acl or otherwise # Accepts any [PSObject] as long as it has an 'Access' property that contains a collection [Parameter( ValueFromPipeline )] [PSObject]$InputObject ) process { ForEach ($ThisInputObject in $InputObject) { $ObjectProperties = @{ SourceAccessList = $ThisInputObject } $AllACEs = $ThisInputObject.Access $AceProperties = (Get-Member -InputObject $AllACEs[0] -MemberType Property, CodeProperty, ScriptProperty, NoteProperty).Name ForEach ($ThisACE in $AllACEs) { ForEach ($ThisProperty in $AceProperties) { $ObjectProperties["$Prefix$ThisProperty"] = $ThisACE.$ThisProperty } [PSCustomObject]$ObjectProperties } } } } function Format-FolderPermission { Param ( # Expects ACEs grouped using Group-Object $UserPermission, # Ignore these FileSystemRights [string[]]$FileSystemRightsToIgnore = @('Synchronize') ) begin { $i = 0 } process { ForEach ($ThisUser in $UserPermission) { $i++ #Calculate the completion percentage, and format it to show 0 decimal places $percentage = "{0:N0}" -f (($i / ($UserPermission.Count)) * 100) #Display the progress bar $status = ("$(Get-Date -Format s)`t$(hostname)`tFormat-FolderPermission`tStatus: " + $percentage + "% - Processing user permission $i of " + $UserPermission.Count + ": " + $ThisUser.Name) Write-Verbose $status Write-Progress -Activity ("Total Users: " + $UserPermission.Count) -Status $status -PercentComplete $percentage ForEach ($ThisACE in $ThisUser.Group.NtfsAccessControlEntries) { switch ($ThisACE.InheritanceFlags) { 'ContainerInherit, ObjectInherit' { $Scope = 'this folder, subfolders, and files' } 'ContainerInherit' { $Scope = 'this folder and subfolders' } 'ObjectInherit' { $Scope = 'this folder and files, but not subfolders' } default { $Scope = 'this folder but not subfolders' } } if ($ThisUser.Group.DirectoryEntry.Properties) { $Name = $ThisUser.Group.DirectoryEntry.Properties['name'] | Sort-Object -Unique $Dept = $ThisUser.Group.DirectoryEntry.Properties['department'] | Sort-Object -Unique $Title = $ThisUser.Group.DirectoryEntry.Properties['title'] | Sort-Object -Unique } else { $Name = $ThisUser.Group.name | Sort-Object -Unique $Dept = $ThisUser.Group.department | Sort-Object -Unique $Title = $ThisUser.Group.title | Sort-Object -Unique } if ($null -eq $ThisUser.Group.IdentityReference) { $IdentityReference = $null } else { $IdentityReference = $ThisACE.IdentityReferenceResolved } $FileSystemRights = $ThisACE.FileSystemRights ForEach ($Ignore in $FileSystemRightsToIgnore) { $FileSystemRights = $FileSystemRights -replace ", $Ignore\Z", '' -replace "$Ignore,", '' } [pscustomobject]@{ Folder = $ThisACE.SourceAccessList.Path FolderInheritanceEnabled = !($ThisACE.SourceAccessList.AreAccessRulesProtected) Access = "$($ThisACE.AccessControlType) $FileSystemRights $Scope" Account = $ThisUser.Name Name = $Name Department = $Dept Title = $Title IdentityReference = $IdentityReference AccessControlEntry = $ThisACE SchemaClassName = $ThisUser.Group.SchemaClassName | Select-Object -First 1 } } } } end { Write-Progress -Activity ("Total User Permissions: " + $UserPermission.Count) -Completed } } function Format-SecurityPrincipal { # Format Security Principals (distinguish group members from principals directly listed in the NTFS DACLs) # The IdentityReference property will be null for any principals directly listed in the NTFS DACLs param ( # Security Principals received from Expand-IdentityReference in the Adsi module $SecurityPrincipal ) ForEach ($ThisPrincipal in $SecurityPrincipal) { # Format and output the security principal $ThisPrincipal | Select-Object -Property @{ Label = 'User' Expression = { $_.Name } }, @{ Label = 'IdentityReference' Expression = { $null } }, @{ Label = 'NtfsAccessControlEntries' Expression = { $_.Group } }, * # Format and output its members if it is a group $ThisPrincipal.Members | <# # Because we have already recursively retrieved all group members, we now have all the users so we can filter out the groups from the group members. Where-Object -FilterScript { if ($_.DirectoryEntry.Properties) { $_.DirectoryEntry.Properties['objectClass'] -notcontains 'group' -and $null -eq $_.DirectoryEntry.Properties['groupType'].Value } else { $_.Properties['objectClass'] -notcontains 'group' -and $null -eq $_.Properties['groupType'].Value } } | #> Select-Object -Property @{ Label = 'User' Expression = { "$($_.Domain.Netbios)\$($_.SamAccountName)" } }, @{ Label = 'IdentityReference' Expression = { $ThisPrincipal.Group.IdentityReference | Sort-Object -Unique } }, @{ Label = 'NtfsAccessControlEntries' Expression = { $ThisPrincipal.Group } }, * } } function Get-FolderAce { <# .SYNOPSIS Alternative to Get-Acl designed to be as lightweight as possible .DESCRIPTION Returns an object for each access control entry instead of a single object for the ACL Excludes inherited permissions by default but allows them to be included with the -IncludeInherited switch parameter .INPUTS [System.String]$LiteralPath .OUTPUTS [PSCustomObject] .NOTES Currently only supports Directories but could easily be copied to support files, or Registry or AD providers #> [CmdletBinding( SupportsShouldProcess = $false, ConfirmImpact = "Low" )] param( # Path to the directory whose permissions to get [string]$LiteralPath, # Include inherited Access Control Entries in the results [Switch]$IncludeInherited, # Include all sections except Audit because it requires admin rights if run on the local system and we want to avoid that requirement [System.Security.AccessControl.AccessControlSections]$Sections = ([System.Security.AccessControl.AccessControlSections]::Access -bor [System.Security.AccessControl.AccessControlSections]::Owner -bor [System.Security.AccessControl.AccessControlSections]::Group), # Include non-inherited Access Control Entries in the results [bool]$IncludeExplicitRules = $true, # Type of IdentityReference to return in each ACE [System.Type]$AccountType = [System.Security.Principal.SecurityIdentifier] ) $DirectorySecurity = & { [System.Security.AccessControl.DirectorySecurity]::new( $LiteralPath, $Sections ) } 2>$null if (-not $DirectorySecurity) { continue } $AclProperties = @{} $AclPropertyNames = (Get-Member -InputObject $DirectorySecurity -MemberType Property, CodeProperty, ScriptProperty, NoteProperty).Name ForEach ($ThisProperty in $AclPropertyNames) { $AclProperties[$ThisProperty] = $DirectorySecurity.$ThisProperty } $AclProperties['Path'] = $LiteralPath $AccessRules = $DirectorySecurity.GetAccessRules($IncludeExplicitRules, $IncludeInherited, $AccountType) $ACEPropertyNames = (Get-Member -InputObject $AccessRules[0] -MemberType Property, CodeProperty, ScriptProperty, NoteProperty).Name ForEach ($ThisAccessRule in $AccessRules) { $ACEProperties = @{ SourceAccessList = [PSCustomObject]$AclProperties } ForEach ($ThisProperty in $ACEPropertyNames) { $ACEProperties[$ThisProperty] = $ThisAccessRule.$ThisProperty } [pscustomobject]$ACEProperties } $ACEProperties['IsInherited'] = $false $ACEProperties['IdentityReference'] = $DirectorySecurity.Owner $ACEProperties['FileSystemRights'] = [System.Security.AccessControl.FileSystemRights]::FullControl $ACEProperties['InheritanceFlags'] = [System.Security.AccessControl.InheritanceFlags]::None $ACEProperties['PropagationFlags'] = [System.Security.AccessControl.PropagationFlags]::None $ACEProperties['AccessControlType'] = [System.Security.AccessControl.AccessControlType]::Allow #TODO: Output an object for the owner as well to represent that they have Full Control [PSCustomObject]$ACEProperties } function Get-FolderTarget { param ( [string[]]$FolderPath ) process { foreach ($TargetPath in $FolderPath) { $RegEx = '^(?<DriveLetter>\w):' if ($TargetPath -match $RegEx) { $TargetPath -replace $RegEx, "\\$(hostname)\$($Matches.DriveLetter)$" } else { #$DFSDetails = [NetApi32Dll]::NetDfsGetInfo($TargetPath) # Can't use this because it doesn't work if the provided path is a subfolder of a DFS folder $AllDfs = Get-NetDfsEnum -Verbose -FolderPath $TargetPath $DfsDetails = $AllDfs | Group-Object -Property DfsEntryPath | Where-Object -FilterScript { "$TargetPath" -like "$($_.Name)\*" } | Sort-Object -Property Name $DfsNamespaceRoot = $DfsDetails | Select-Object -First 1 $DfsDetails | Select-Object -Last 1 -ExpandProperty Group | ForEach-Object { $_.FullOriginalQueryPath -replace [regex]::Escape($_.DfsEntryPath), $_.DfsTarget } } } } } function Get-Subfolder { # Use the fastest available method to enumerate subfolders [CmdletBinding()] param ( # Parent folder whose subfolders to enumerate [string]$TargetPath, <# How many levels of subfolder to enumerate Set to 0 to ignore all subfolders Set to -1 (default) to recurse infinitely Set to any whole number to enumerate that many levels #> [int]$FolderRecursionDepth = -1 ) if ($FolderRecursionDepth -eq -1) { $DepthString = '∞' } else { $DepthString = $FolderRecursionDepth } Write-Progress -Activity ("Retrieving subfolders...") -Status ("Enumerating all subfolders of '$TargetPath' to a depth of $DepthString levels of recursion") -PercentComplete 50 if ($Host.Version.Major -gt 2) { switch ($FolderRecursionDepth) { -1 { GetDirectories -TargetPath $TargetPath -SearchOption ([System.IO.SearchOption]::AllDirectories) } 0 {} 1 { GetDirectories -TargetPath $TargetPath -SearchOption ([System.IO.SearchOption]::TopDirectoryOnly) } Default { $FolderRecursionDepth = $FolderRecursionDepth - 1 Write-Debug " $(Get-Date -Format s)`t$(hostname)`tGet-Subfolder`tGet-ChildItem '$TargetPath' -Force -Name -Recurse -Attributes Directory -Depth $FolderRecursionDepth" (Get-ChildItem $TargetPath -Force -Recurse -Attributes Directory -Depth $FolderRecursionDepth).FullName } } } else { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tGet-Subfolder`tGet-ChildItem '$TargetPath' -Recurse" Get-ChildItem $TargetPath -Recurse | Where-Object -FilterScript { $_.PSIsContainer } | ForEach-Object { $_.FullName } } Write-Progress -Activity ("Retrieving subfolders...") -Completed } function New-NtfsAclIssueReport { param ( $FolderPermissions, $UserPermissions, <# If specified, all groups that have NTFS access to the target folder/subfolders will be evaluated for compliance with this naming convention The naming format that will be used for the users is CONTOSO\User1 where CONTOSO is the NetBIOS name of the domain, and User1 is the samAccountName of the user By default, this is a scriptblock that always evaluates to $true so it doesn't evaluate any naming convention compliance #> [scriptblock]$GroupNamingConvention = { $true } ) $IssuesDetected = $false # List of folders with broken inheritance (recommend moving to higher level to avoid breaking inheritance. Deny entries are a less desirable alternative) $FoldersWithBrokenInheritance = $FolderPermissions | Select-Object -Skip 1 | Where-Object -FilterScript { ($_.Group.FolderInheritanceEnabled | Select-Object -First 1) -eq $false -and (($_.Name -replace ([regex]::Escape($TargetPath)), '' -split '\\') | Measure-Object).Count -ne 2 } $Count = ($FoldersWithBrokenInheritance | Measure-Object).Count if ($Count -gt 0) { $IssuesDetected = $true $Txt = "folders with broken inheritance: $($FoldersWithBrokenInheritance.Name -join "`r`n")" } else { $Txt = 'OK' } Write-Verbose "$Count`:$Txt" # List of ACEs for groups that do not match the specified naming convention # Invert the naming convention scriptblock (because we actually want to identify groups that do NOT follow the convention) $ViolatesNamingConvention = [scriptblock]::Create("!($GroupNamingConvention)") $NonCompliantGroups = $SecurityPrincipals | Where-Object -FilterScript { $_.ObjectType -contains 'Group' } | Where-Object -FilterScript $ViolatesNamingConvention | Select-Object -ExpandProperty Group | ForEach-Object { "$($_.IdentityReference) on '$($_.Path)'" } $Count = ($NonCompliantGroups | Measure-Object).Count if ($Count -gt 0) { $IssuesDetected = $true $Txt = "groups that don't match naming convention: $($NonCompliantGroups -join "`r`n")" } else { $Txt = 'OK' } Write-Verbose "$Count`:$Txt" # ACEs for users (recommend replacing with group-based access on any folder that is not a home folder) $UserACEs = $UserPermissions.Group | Where-Object { $_.ObjectType -contains 'User' } | ForEach-Object { $_.NtfsAccessControlEntries } | ForEach-Object { "$($_.IdentityReference) on '$($_.Path)'" } | Sort-Object -Unique $Count = ($UserACEs | Measure-Object).Count if ($Count -gt 0) { $IssuesDetected = $true $Txt = "users with ACEs: $($UserACEs -join "`r`n")" } else { $Txt = 'OK' } Write-Verbose "$Count`:$Txt" # ACEs for unresolvable SIDs (recommend removing these ACEs) $SIDsToCleanup = $UserPermissions.Group.NtfsAccessControlEntries | Where-Object -FilterScript { $_.IdentityReference -match 'S-\d+-\d+-\d+-\d+-\d+\-\d+\-\d+' } | ForEach-Object { "$($_.IdentityReference) on '$($_.Path)'" } | Sort-Object -Unique $Count = ($SIDsToCleanup | Measure-Object).Count if ($Count -gt 0) { $IssuesDetected = $true $Txt = "ACEs for unresolvable SIDs: $($SIDsToCleanup -join "`r`n")" } else { $Txt = 'OK' } Write-Verbose "$Count`:$Txt" # CREATOR OWNER access (recommend replacing with group-based access, or with explicit user access for a home folder.) $FoldersWithCreatorOwner = ($UserPermissions | ? { $_.Name -match 'CREATOR OWNER' }).Group.NtfsAccessControlEntries.Path | Sort -Unique $Count = ($FoldersWithCreatorOwner | Measure-Object).Count if ($Count -gt 0) { $IssuesDetected = $true $Txt = "folders with 'CREATOR OWNER' ACEs: $($FoldersWithCreatorOwner -join "`r`n")" } else { $Txt = 'OK' } Write-Verbose "$Count`:$Txt" [PSCustomObject]@{ IssueDetected = $IssuesDetected FoldersWithBrokenInheritance = $FoldersWithBrokenInheritance NonCompliantGroups = $NonCompliantGroups UserACEs = $UserACEs SIDsToCleanup = $SIDsToCleanup FoldersWithCreatorOwner = $FoldersWithCreatorOwner } } function Remove-DuplicatesAcrossIgnoredDomains { param ( [Parameter(ValueFromPipeline)] $UserPermission, [string[]]$DomainToIgnore ) begin { $KnownUsers = [hashtable]::Synchronized(@{}) } process { ForEach ($ThisUser in $UserPermission) { $ShortName = $ThisUser.Name ForEach ($IgnoreThisDomain in $DomainToIgnore) { $ShortName = $ShortName -replace $IgnoreThisDomain,'' } if ($null -eq $KnownUsers[$ShortName]) { $KnownUsers[$ShortName] = [pscustomobject]@{ 'Name' = $ShortName 'Group' = $ThisUser.Group } } else { $KnownUsers[$ShortName] = [pscustomobject]@{ 'Name' = $ShortName 'Group' = $KnownUsers[$ShortName].Group + $ThisUser.Group } } } } end { $KnownUsers.Values | Sort-Object -Property Name } } <# # Dot source any functions ForEach ($ThisScript in $ScriptFiles) { # Dot source the function . $($ThisScript.FullName) } #> # Definition of Module 'PsLogMessage' is below function New-DatedSubfolder { # Creates a folder structure with a folder for each year and month # Then it creates one timestamped folder inside the appropriate month # This folder is intended to be used to store output from a single execution of a script param ( [parameter(Mandatory)] [string]$Root ) $Year = Get-Date -Format 'yyyy' $Month = Get-Date -Format 'MM' $Timestamp = (Get-Date -Format s) -replace ':', '-' $NewDir = "$Root\$Year\$Month\$Timestamp" $null = New-Item -ItemType Directory -Path $NewDir -ErrorAction SilentlyContinue Write-Output $NewDir } function Write-LogMsg { <# .SYNOPSIS Prepend a prefix to a log message, write the message to an output stream, and write the message to a text file. Writes a message to a log file and/or PowerShell output stream .DESCRIPTION Prepends the log message with: a current timestamp the current hostname the current username the current command (function or file name) the current location (line number in the code) Tab-delimits these fields for a compromise between readability and parseability Adds the log message to a Global variable #TODO: Make this a thread-safe hashtable, using the timestamp as the key Optionally writes the message to a log file Optionally writes the message to a PowerShell output stream .NOTES #> [CmdletBinding()] param( # Message to log [string]$Text, # Output stream to send the message to [string]$Type = 'Information', # Add a prefix to the message including the date, hostname, current user, and info about the current call stack [bool]$AddPrefix = $true, # Text file to append the log message to [string]$LogFile, # Output the message to the pipeline [bool]$PassThru = $false ) $Timestamp = Get-Date -Format s $OutputToPipeline = $false $ThisHostname = HOSTNAME.EXE $WhoAmI = whoami.exe $PSCallStack = Get-PSCallStack if ($AddPrefix) { # This method is faster than StringBuilder or the -join operator [string]$MessageToLog = "$Timestamp`t$ThisHostname`t$WhoAmI`t$($PSCallStack[1].Location)`t$($PSCallStack[1].Command)`t$($MyInvocation.ScriptLineNumber)`t$($Type)`t$($Text)" } else { [string]$MessageToLog = $Text } Switch ($Type) { # This will ensure the message is not written to any PowerShell output streams 'Silent' {} # This one is made-up to correspond with the 'success' contextual class in Bootstrap. 'Success' { Write-Information "SUCCESS: $MessageToLog" } # These represent normal PowerShell output streams # The correct number of spaces should be added to maintain proper column alignment 'Debug' { Write-Debug " $MessageToLog" } 'Verbose' { Write-Verbose $MessageToLog } 'Host' { Write-Host "HOST: $MessageToLog" } 'Warning' { Write-Warning $MessageToLog } 'Error' { Write-Error $MessageToLog } 'Output' { $OutputToPipeline = $true } default { Write-Information "INFO: $MessageToLog" } } if ('' -ne $LogFile) { $MessageToLog | Out-File $LogFile -Append } if ($PassThru -or $OutputToPipeline) { $MessageToLog } # Add a GUID to the timestamp and use it as a unique key in the hashtable of log messages [string]$Guid = [guid]::NewGuid() [string]$Key = "$Timestamp$Guid" $Global:LogMessages[$Key] = [pscustomobject]@{ Timestamp = $Timestamp Hostname = $ThisHostname WhoAmI = $WhoAmI Location = $PSCallStack[1].Location Command = $PSCallStack[1].Command Line = $MyInvocation.ScriptLineNumber Type = $Type Text = $Text } } <# # Add any custom C# classes as usable (exported) types $CSharpFiles = Get-ChildItem -Path "$PSScriptRoot\*.cs" ForEach ($ThisFile in $CSharpFiles) { Add-Type -Path $ThisFile.FullName -ErrorAction Stop } #> #$Global:LogMessages = [system.collections.generic.list[pscustomobject]]::new() $Global:LogMessages = [hashtable]::Synchronized(@{}) # Definition of Module 'PsRunspace' is below function Add-PsCommand { <# .Synopsis Add a command to a [System.Management.Automation.PowerShell] instance .Description Used by Invoke-Thread Uses AddScript() or AddStatement() and AddCommand() depending on the command .EXAMPLE The following demonstrates sending a Cmdlet name to the -Command parameter [powershell]::Create() | AddCommand -Command 'Write-Output' #> param( # Powershell interface to add the Command to [Parameter(ValueFromPipeline = $true)] [powershell[]]$PowershellInterface, <# Command to add to the Powershell interface This can be a scriptblock object, or a string that specifies a: Alias Function (the name of the function) ExternalScript (the path to the .ps1 file) All, Application, Cmdlet, Configuration, Filter, or Script #> [Parameter(Position = 0)] $Command, # Output from Get-PsCommandInfo # Optional, to improve performance if it will be re-used for multiple calls of Add-PsCommand [pscustomobject]$CommandInfo ) begin { [int64]$CurrentObjectIndex = 0 if ($CommandInfo -eq $null) { $CommandInfo = Get-PsCommandInfo -Command $Command } } process { ForEach ($ThisPowershell in $PowershellInterface) { switch ($CommandInfo.CommandType) { 'Alias' { # Resolve the alias to its command and start from the beginning with that command. $CommandInfo = Get-PsCommandInfo -Command $CommandInfo.CommandInfo.Definition Add-PsCommand -Command $CommandInfo.CommandInfo.Definition -CommandInfo $CommandInfo -PowershellInterface $ThisPowerShell } 'Function' { $ThisPowershell.AddScript($CommandInfo.CommandInfo.Definition) } 'ScriptBlock' { $ThisPowershell.AddScript($Command) } 'ExternalScript' { $ThisPowershell.AddScript($CommandInfo.ScriptBlock) } default { # If the type is All, Application, Cmdlet, Configuration, Filter, or Script then run the command as-is $ThisPowershell.AddStatement().AddCommand($Command) } } } } } function Get-PsCommandInfo { <# .Synopsis Get info about a PowerShell command .Description Used by Split-Thread, Invoke-Thread, and Add-PsCommand Determine whether the Command is a [System.Management.Automation.ScriptBlock] object If not, passes it to the Name parameter of Get-Command .EXAMPLE The following demonstrates sending a Cmdlet name to the -Command parameter Get-PsCommandInfo -Command 'Write-Output' #> param( <# Command to retrieve info on This can be a scriptblock object, or a string that specifies an: Alias Function (the name of the function) ExternalScript (the path to the .ps1 file) All, Application, Cmdlet, Configuration, Filter, or Script #> $Command ) if ($Command.GetType().FullName -eq 'System.Management.Automation.ScriptBlock') { $CommandType = 'ScriptBlock' } else { $CommandInfo = Get-Command $Command -ErrorAction SilentlyContinue $CommandType = $CommandInfo.CommandType if ($CommandInfo.Source) { $ModuleInfo = Get-Module -Name $CommandInfo.Source -ErrorAction SilentlyContinue } } #CommentedForPerformanceOptimization#Write-Debug " $(Get-Date -Format s)`t$(hostname)`tGet-PsCommandInfo`t$Command is a $CommandType" [pscustomobject]@{ CommandInfo = $CommandInfo ModuleInfo = $ModuleInfo CommandType = $CommandType SourceModuleDefinition = $ModuleInfo.Definition SourceModuleName = $CommandInfo.Source } } function Open-Thread { <# .Synopsis Prepares each thread so it is ready to execute a command and capture the output streams .Description Used by Split-Thread For each InputObject an instance will be created of [System.Management.Automation.PowerShell] Then a series of commands will be run to enable the specified output streams (all by default) #> Param( # Objects to pass to the Command as an argument or parameter [Parameter( ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true )] $InputObject, # .Net Framework runspace pool to use for the threads [Parameter( Mandatory = $true )] [System.Management.Automation.Runspaces.RunspacePool]$RunspacePool, <# Name of a property (whose value is a string) that exists on each $InputObject It will be used to represent the object in text form If left null, the object's ToString() method will be used instead. #> [string]$ObjectStringProperty, # PowerShell Command or Script to run against each InputObject [Parameter(Mandatory = $true)] $Command, # Output from Get-PsCommandInfo [pscustomobject]$CommandInfo, # Named parameter of the Command to pass InputObject to # If this is not specified, InputObject will be passed to the Command as an argument [string]$InputParameter = $null, <# Parameters to add to the Command Each parameter is a name-value pair in the hashtable: @{"ParameterName" = "Value"} @{"ParameterName" = "Value" ; "ParameterTwo" = "Value2"} #> [HashTable]$AddParam = @{}, # Switches to add to the Command [string[]]$AddSwitch = @() ) begin { [int64]$CurrentObjectIndex = 0 $ThreadCount = @($InputObject).Count } process { ForEach ($Object in $InputObject) { $CurrentObjectIndex++ if ($ObjectStringProperty -ne '') { $ObjectString = $Object."$ObjectStringProperty" } else { $ObjectString = $Object.ToString() } $PowershellInterface = [powershell]::Create() $PowershellInterface.RunspacePool = $RunspacePool $null = $PowershellInterface.Commands.Clear() Add-PsCommand -Command $Command -CommandInfo $CommandInfo -PowershellInterface $PowershellInterface If (!([string]::IsNullOrEmpty($InputParameter))) { $null = $PowershellInterface.AddParameter($InputParameter, $Object) <#NormallyCommentThisForPerformanceOptimization#>$InputParameterString = "-$InputParameter '$ObjectString'" } Else { $null = $PowershellInterface.AddArgument($Object) <#NormallyCommentThisForPerformanceOptimization#>$InputParameterString = "'$ObjectString'" } $AdditionalParameters = @() $AdditionalParameters = ForEach ($Key in $AddParam.Keys) { $null = $PowershellInterface.AddParameter($Key, $AddParam.$key) <#NormallyCommentThisForPerformanceOptimization#>"-$Key '$($AddParam.$key)'" } $AdditionalParametersString = $AdditionalParameters -join ' ' $Switches = @() $Switches = ForEach ($Switch in $AddSwitch) { $null = $PowershellInterface.AddParameter($Switch) <#NormallyCommentThisForPerformanceOptimization#>"-$Switch" } $SwitchParameterString = $Switches -join ' ' $StatusString = "Invoking thread $CurrentObjectIndex`: $Command $InputParameterString $AdditionalParametersString $SwitchParameterString" <#NormallyCommentThisForPerformanceOptimization#>#Write-Debug " $(Get-Date -Format s)`t$(hostname)`tOpen-Thread`t$StatusString" $Progress = @{ Activity = $StatusString PercentComplete = $CurrentObjectIndex / $ThreadCount * 100 Status = "$($ThreadCount - $CurrentObjectIndex) remaining" } Write-Progress @Progress $Handle = $PowershellInterface.BeginInvoke() [PSCustomObject]@{ Handle = $Handle PowerShellInterface = $PowershellInterface Object = $Object ObjectString = $ObjectString Index = $CurrentObjectIndex Command = "$Command" } } } end { Write-Progress -Activity 'Completed' -Completed } } function Split-Thread { <# .Synopsis Split a command for a collection of input objects into multiple threads for asynchronous processing .Description The specified command will be run for each input object in a separate powershell instance with its own runspace These runspaces are part of the same runspace pool inside the same powershell.exe process .EXAMPLE The following demonstrates sending a Cmdlet name to the -Command parameter $InputObject | Split-Thread -Command 'Write-Output' .EXAMPLE The following demonstrates sending a scriptblock to the -Command parameter $InputObject | Split-Thread -Command [scriptblock]::create("Write-Output `$args[0]") .EXAMPLE The following demonstrates sending a script file path to the -Command parameter $InputObject | Split-Thread -Command "C:\Test-Command.ps1" .EXAMPLE The following demonstrates sending a function to the -Command parameter $InputObject | Split-Thread -Command 'Test-Function' .EXAMPLE The following demonstrates the -AddParam parameter $InputObject | Split-Thread -Command "Get-Service" -InputParameter ComputerName -AddParam @{"Name" = "BITS"} .EXAMPLE The following demonstrates the -AddSwitch parameter $InputObject | Split-Thread -Command "Get-Service" -AddSwitch @('RequiredServices','DependentServices') .EXAMPLE The following demonstrates the use of a threadsafe hashtable to store results The hastable can be accessed and updated from inside each runspace $ThreadsafeHashtable = [hashtable]::Synchronized(@{}) $InputObject | Split-Thread -Command "Fake-Function" -InputParameter ComputerName -AddParam @{"ResultHashTableParameter" = $ThreadsafeHashtable} #> param ( # PowerShell Command or Script to run against each InputObject [Parameter(Mandatory = $true)] $Command, # Objects to pass to the Command as an argument or parameter [Parameter( ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true )] $InputObject, # Named parameter of the Command to pass InputObject to # If this is not specified, InputObject will be passed to the Command as an argument $InputParameter = $null, # Maximum number of concurrent threads to allow [int]$Threads = 20, # Milliseconds to wait between cycles of the loop that checks threads for completion [int]$SleepTimer = 200, # Seconds to wait without receiving any new results before giving up and stopping all remaining threads [int]$Timeout = 120, <# Parameters to add to the Command Each parameter is a name-value pair in the hashtable: @{"ParameterName" = "Value"} @{"ParameterName" = "Value" ; "ParameterTwo" = "Value2"} #> [HashTable]$AddParam = @{}, # Switches to add to the Command [string[]]$AddSwitch = @(), # Names of modules to import in each runspace [String[]]$AddModule, <# Name of a property (whose value is a string) that exists on each $InputObject and can be used to represent the object in text form If left null, the object's ToString() method will be used instead. #> [string]$ObjectStringProperty ) begin { $InitialSessionState = [system.management.automation.runspaces.initialsessionstate]::CreateDefault() # Import the source module containing the specified Command in each thread $CommandInfo = Get-PsCommandInfo -Command $Command switch ($CommandInfo.ModuleInfo.ModuleType) { 'Binary' { Write-Debug "`$InitialSessionState.ImportPSModule('$($CommandInfo.ModuleInfo.Name)')" $InitialSessionState.ImportPSModule($CommandInfo.ModuleInfo.Name) } 'Script' { $ModulePath = $CommandInfo.ModuleInfo.Path | Split-Path -Parent Write-Debug "`$InitialSessionState.ImportPSModulesFromPath('$ModulePath')" $InitialSessionState.ImportPSModulesFromPath($ModulePath) } 'Manifest' { $ModulePath = $CommandInfo.ModuleInfo.Path | Split-Path -Parent Write-Debug "`$InitialSessionState.ImportPSModulesFromPath('$ModulePath')" $InitialSessionState.ImportPSModulesFromPath($ModulePath) } default { # Scriptblocks have no module to import so ModuleInfo will be null } } # Import any additional specified modules in each thread ForEach ($Module in $AddModule) { $ModuleObj = Get-Module $Module -ErrorAction SilentlyContinue switch ($ModuleObj.ModuleType) { 'Binary' { Write-Debug "`$InitialSessionState.ImportPSModule('$Module')" $InitialSessionState.ImportPSModule($Module) } default { # This is for Script or Manifest modules Write-Debug "`$InitialSessionState.ImportPSModulesFromPath('$($ModuleObj.Path | Split-Path -Parent)')" $InitialSessionState.ImportPSModulesFromPath(($ModuleObj.Path | Split-Path -Parent)) } } } # Set the preference variables for PowerShell output streams in each thread to match the current preferences $OutputStream = @('Debug', 'Verbose', 'Information', 'Warning', 'Error') ForEach ($ThisStream in $OutputStream) { if ($ThisStream -eq 'Error') { $VariableName = 'ErrorActionPreference' } else { $VariableName = "$($ThisStream)Preference" } $VariableValue = (Get-Variable -Name $VariableName).Value $VariableEntry = [System.Management.Automation.Runspaces.SessionStateVariableEntry]::new($VariableName, $VariableValue, '') $InitialSessionState.Variables.Add($VariableEntry) } $RunspacePool = [runspacefactory]::CreateRunspacePool(1, $Threads, $InitialSessionState, $Host) $VerbosePreference = 'SilentlyContinue' $RunspacePool.Open() $Global:TimedOut = $false $AllInputObjects = [System.Collections.Generic.List[psobject]]::new() } process { # Add all the input objects from the pipeline to a single collection; allows progress bars later ForEach ($ThisObject in $InputObject) { $null = $AllInputObjects.Add($ThisObject) } } end { $ThreadParameters = @{ Command = $Command InputParameter = $InputParameter InputObject = $AllInputObjects AddParam = $AddParam AddSwitch = $AddSwitch ObjectStringProperty = $ObjectStringProperty CommandInfo = $CommandInfo RunspacePool = $RunspacePool } $AllThreads = Open-Thread @ThreadParameters Wait-Thread -Thread $AllThreads -Threads $Threads -SleepTimer $SleepTimer -Timeout $Timeout -Dispose $VerbosePreference = 'Continue' if ($Global:TimedOut -eq $false) { #CommentedForPerformanceOptimization#Write-Debug " $(Get-Date -Format s)`t$(hostname)`tSplit-Thread`t[System.Management.Automation.Runspaces.RunspacePool]::Close()" $null = $RunspacePool.Close() #CommentedForPerformanceOptimization#Write-Debug " $(Get-Date -Format s)`t$(hostname)`tSplit-Thread`t[System.Management.Automation.Runspaces.RunspacePool]::Close() completed" #CommentedForPerformanceOptimization#Write-Debug " $(Get-Date -Format s)`t$(hostname)`tSplit-Thread`t[System.Management.Automation.Runspaces.RunspacePool]::Dispose()" $null = $RunspacePool.Dispose() #CommentedForPerformanceOptimization#Write-Debug " $(Get-Date -Format s)`t$(hostname)`tSplit-Thread`t[System.Management.Automation.Runspaces.RunspacePool]::Dispose() completed" } Write-Progress -Activity 'Completed' -Completed } } function Wait-Thread { <# .Synopsis Waits for a thread to be completed so the results can be returned, or for a timeout to be reached .Description Used by Split-Thread .INPUTS [PSCustomObject]$Thread .OUTPUTS Outputs the specified output streams from the threads #> param ( # Threads to wait for [Parameter( Mandatory = $true, ValueFromPipeline = $true )] [PSCustomObject[]]$Thread, # Maximum number of concurrent threads that are allowed (used only for progress display) [int]$Threads = 20, # Milliseconds to wait between cycles of the loop that checks threads for completion [int]$SleepTimer = 200, # Seconds to wait without receiving any new results before giving up and stopping all remaining threads [int]$Timeout = 120, # Dispose of the thread when it is finished [switch]$Dispose ) begin { $StopWatch = [System.Diagnostics.Stopwatch]::new() $StopWatch.Start() $AllThreads = [System.Collections.Generic.List[PSCustomObject]]::new() $RunspacePool = ($Thread | Select-Object -First 1).PowershellInterface.RunspacePool $CommandString = ($Thread | Select-Object -First 1).Command } process { ForEach ($ThisThread in $Thread) { # If the threads do not have handles, there is nothing to wait for, so output the thread as-is. # Otherwise wait for the handle to indicate completion (or a timeout to be reached) if ($ThisThread.Handle -eq $false) { $null = $ThisThread.PowerShellInterface.Streams.ClearStreams() $ThisThread } else { $null = $AllThreads.Add($ThisThread) } } } end { # If the threads have handles, we can check to see if they are complete. While (@($AllThreads | Where-Object -FilterScript { $null -ne $_.Handle }).Count -gt 0) { if ($RunspacePool) { $AvailableRunspaces = $RunspacePool.GetAvailableRunspaces() } $CleanedUpThreads = [System.Collections.Generic.List[PSCustomObject]]::new() $CompletedThreads = [System.Collections.Generic.List[PSCustomObject]]::new() $IncompleteThreads = [System.Collections.Generic.List[PSCustomObject]]::new() ForEach ($ThisThread in $AllThreads) { if ($null -eq $ThisThread.Handle) { $null = $CleanedUpThreads.Add($ThisThread) } if ($ThisThread.Handle.IsCompleted -eq $true) { $null = $CompletedThreads.Add($ThisThread) } if ($ThisThread.Handle.IsCompleted -eq $false) { $null = $IncompleteThreads.Add($ThisThread) } } $ActiveThreadCountString = "$($Threads - $AvailableRunspaces) of $Threads are active" #CommentedForPerformanceOptimization#Write-Debug " $(Get-Date -Format s)`t$(hostname)`tWait-Thread`t$ActiveThreadCountString" #CommentedForPerformanceOptimization#Write-Debug " $(Get-Date -Format s)`t$(hostname)`tWait-Thread`t$($CompletedThreads.Count) completed threads" #CommentedForPerformanceOptimization#Write-Debug " $(Get-Date -Format s)`t$(hostname)`tWait-Thread`t$($CleanedUpThreads.Count) cleaned up threads" #CommentedForPerformanceOptimization#Write-Debug " $(Get-Date -Format s)`t$(hostname)`tWait-Thread`t$($IncompleteThreads.Count) incomplete threads" $RemainingString = "$($IncompleteThreads.ObjectString)" If ($RemainingString.Length -gt 60) { $RemainingString = $RemainingString.Substring(0, 60) + "..." } $Progress = @{ Activity = "Waiting on threads - $ActiveThreadCountString`: $CommandString" PercentComplete = ($($CleanedUpThreads).count) / @($Thread).Count * 100 Status = "$(@($IncompleteThreads).Count) remaining - $RemainingString" } Write-Progress @Progress ForEach ($CompletedThread in $CompletedThreads) { #Write-Debug " $(Get-Date -Format s)`t$(hostname)`tWait-Thread`t$($CompletedThread.PowerShellInterface.Streams.Progress.Count) Progress messages" #Write-Debug " $(Get-Date -Format s)`t$(hostname)`tWait-Thread`t$($CompletedThread.PowerShellInterface.Streams.Information.Count) Information messages" #Write-Debug " $(Get-Date -Format s)`t$(hostname)`tWait-Thread`t$($CompletedThread.PowerShellInterface.Streams.Verbose.Count) Verbose messages" #Write-Debug " $(Get-Date -Format s)`t$(hostname)`tWait-Thread`t$($CompletedThread.PowerShellInterface.Streams.Debug.Count) Debug messages" #Write-Debug " $(Get-Date -Format s)`t$(hostname)`tWait-Thread`t$($CompletedThread.PowerShellInterface.Streams.Warning.Count) Warning messages" # Because $Host was used to create the RunspacePool, any output to $Host (which includes Write-Host and Write-Information and Write-Progress) has already been displayed #$CompletedThread.PowerShellInterface.Streams.Progress | ForEach-Object {Write-Progress $_} #$CompletedThread.PowerShellInterface.Streams.Information | ForEach-Object {Write-Information $_} #$CompletedThread.PowerShellInterface.Streams.Verbose | ForEach-Object {Write-Verbose $_} #$CompletedThread.PowerShellInterface.Streams.Debug | ForEach-Object {Write-Debug $_} #$CompletedThread.PowerShellInterface.Streams.Warning | ForEach-Object {Write-Warning $_} $null = $CompletedThread.PowerShellInterface.Streams.ClearStreams() if ($Dispose -eq $true) { $ThreadOutput = $CompletedThread.PowerShellInterface.EndInvoke($CompletedThread.Handle) #NormallyCommentThisForPerformanceOptimization#>if (($ThreadOutput | Measure-Object).Count -gt 0) { #NormallyCommentThisForPerformanceOptimization#>Write-Debug " $(Get-Date -Format s)`t$(hostname)`tWait-Thread`tOutput (count of $($ThreadOutput.Count)) received from thread $($CompletedThread.Index): $($CompletedThread.ObjectString)" #NormallyCommentThisForPerformanceOptimization#>} #NormallyCommentThisForPerformanceOptimization#>else { #NormallyCommentThisForPerformanceOptimization#>Write-Debug " $(Get-Date -Format s)`t$(hostname)`tWait-Thread`tNull result for thread $($CompletedThread.Index) ($($CompletedThread.ObjectString))" #NormallyCommentThisForPerformanceOptimization#>} $ThreadOutput $null = $CompletedThread.PowerShellInterface.Dispose() $CompletedThread.PowerShellInterface = $null $CompletedThread.Handle = $null } else { #CommentedForPerformanceOptimization#Write-Debug " $(Get-Date -Format s)`t$(hostname)`tWait-Thread`tThread $($CompletedThread.Index) ($($CompletedThread.ObjectString)) is finished opening." $null = $CompletedThread.PowerShellInterface.EndInvoke($CompletedThread.Handle) $CompletedThread.Handle = $null $CompletedThread } $StopWatch.Reset() $StopWatch.Start() } If ($StopWatch.ElapsedMilliseconds / 1000 -gt $Timeout) { Write-Warning "$(Get-Date -Format s)`t$(hostname)`tWait-Thread`tReached Timeout of $Timeout seconds. Skipping $($IncompleteThreads.Count) remaining threads: $RemainingString" $Global:TimedOut = $true $IncompleteThreads | ForEach-Object { [PSCustomObject]@{ Handle = $null PowerShellInterface = $_.PowershellInterface Object = $_.Object ObjectString = $_.ObjectString Index = $_.CurrentObjectIndex Command = $_.Command } } } #CommentedForPerformanceOptimization#Write-Debug " $(Get-Date -Format s)`t$(hostname)`tWait-Thread`tSleeping $SleepTimer milliseconds" Start-Sleep -Milliseconds $SleepTimer } $StopWatch.Stop() #CommentedForPerformanceOptimization#Write-Verbose "$(Get-Date -Format s)`t$(hostname)`tWait-Thread`tFinished waiting for threads" Write-Progress -Activity 'Completed' -Completed } } <# # Dot source any functions ForEach ($ThisScript in $ScriptFiles) { # Dot source the function . $($ThisScript.FullName) } #> # Definition of Module 'PsDfs' is below Function Get-DfsNetInfo { # Wrapper for the NetDfsGetInfo([string]) method in the lmdfs.h header in NetApi32.dll for Distributed File Systems [CmdletBinding()] Param ( [PSCredential]$Credentials, [Parameter(Mandatory, ValueFromPipeline)] [ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })] [String[]]$FolderPath ) Process { foreach ($ThisFolderPath in $FolderPath) { $Split = $ThisFolderPath -split '\\' $ServerOrDomain = $Split[0] $DfsNamespace = $Split[1] $DfsLink = "" $Remainder = "" <# # Use the NetDfsGetInfo method instead as it does not filter out disabled folder targets # But it does not work #> #[NetApi32Dll]::NetDfsGetClientInfo($ThisFolderPath) #[NetApi32Dll]::NetDfsEnum($ThisFolderPath) [NetApi32Dll]::NetDfsGetInfo($ThisFolderPath) } } } function Get-FileShareInfo { # Get the corresponding local file path for DFS folder targets (which are UNC paths) param ( [Parameter(ValueFromPipeline)] [psobject[]]$ServerAndShare ) process { # State 6 notes that the DFS path is online and active #$DFS = $DfsNetClientInfo #| Where-Object -FilterScript { $_.State -eq 6 } ForEach ($DFS in $ServerAndShare) { $SessionParams = @{ #Credential = $Credentials ComputerName = $DFS.ServerName SessionOption = New-CimSessionOption -Protocol Dcom } $CimParams = @{ CimSession = New-CimSession @SessionParams ClassName = 'Win32_Share' } $ShareName = ($DFS.ShareName -split '\\')[0] $ShareLocalPath = Get-CimInstance @CimParams | Where-Object Name -EQ $ShareName $LocalPath = $DFS.ShareName -replace [regex]::Escape("$ShareName\"), $ShareLocalPath.Path $DFS | Add-Member -PassThru -NotePropertyMembers @{ #DfsPath = $DFS.DfsPath FolderTarget = "$($DFS.ServerName)\$($DFS.ShareName)\$($DFS.DfsPath -replace [regex]::Escape($DFS.ShareName))" #DfsState = $DFS.State #ServerName = $DFS.ServerName #ShareName = $DFS.ShareName LocalPath = $LocalPath } } } } Function Get-NetDfsEnum { # Wrapper for the NetDfsEnum([string]) method in the lmdfs.h header in NetApi32.dll for Distributed File Systems [CmdletBinding()] Param ( [PSCredential]$Credentials, [Parameter(Mandatory, ValueFromPipeline)] [ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })] [String[]]$FolderPath ) Process { foreach ($ThisFolderPath in $FolderPath) { $Split = $ThisFolderPath -split '\\' $ServerOrDomain = $Split[0] $DfsNamespace = $Split[1] $DfsLink = "" $Remainder = "" <# # Use the NetDfsGetInfo method instead as it does not filter out disabled folder targets # But it does not work #> #[NetApi32Dll]::NetDfsGetClientInfo($ThisFolderPath) [NetApi32Dll]::NetDfsEnum($ThisFolderPath) #[NetApi32Dll]::NetDfsGetInfo($ThisFolderPath) } } } Add-Type -ErrorAction Stop -TypeDefinition @" using System; using System.Collections.Generic; using System.ComponentModel; using System.Management.Automation; using System.Runtime.InteropServices; public class NetApi32Dll { [DllImport("netapi32.dll", SetLastError = true)] private static extern int NetApiBufferFree ( IntPtr buffer ); [DllImport("netapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern int NetDfsEnum ( [MarshalAs(UnmanagedType.LPWStr)] string DfsName, int Level, int PrefMaxLen, out IntPtr Buffer, [MarshalAs(UnmanagedType.I4)] out int EntriesRead, [MarshalAs(UnmanagedType.I4)] ref int ResumeHandle ); [DllImport("netapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern int NetDfsGetClientInfo ( [MarshalAs(UnmanagedType.LPWStr)] string EntryPath, [MarshalAs(UnmanagedType.LPWStr)] string ServerName, [MarshalAs(UnmanagedType.LPWStr)] string ShareName, int Level, ref IntPtr Buffer ); [DllImport("netapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern int NetDfsGetInfo ( [MarshalAs(UnmanagedType.LPWStr)] string EntryPath, [MarshalAs(UnmanagedType.LPWStr)] string ServerName, [MarshalAs(UnmanagedType.LPWStr)] string ShareName, int Level, ref IntPtr Buffer ); public struct DFS_INFO_3 { [MarshalAs(UnmanagedType.LPWStr)] public string EntryPath; [MarshalAs(UnmanagedType.LPWStr)] public string Comment; public UInt32 State; public UInt32 NumberOfStorages; public IntPtr Storages; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct DFS_INFO_6 { [MarshalAs(UnmanagedType.LPWStr)] public string EntryPath; [MarshalAs(UnmanagedType.LPWStr)] public string Comment; public UInt32 State; public UInt64 Timeout; public Guid Guid; public UInt32 NumberOfStorages; public UInt64 MetadataSize; public UInt64 PropertyFlags; public IntPtr Storages; } public struct DFS_STORAGE_INFO { public Int32 State; [MarshalAs(UnmanagedType.LPWStr)] public string ServerName; [MarshalAs(UnmanagedType.LPWStr)] public string ShareName; } public struct DFS_STORAGE_INFO_1 { public DFS_STORAGE_STATE State; [MarshalAs(UnmanagedType.LPWStr)] public string ServerName; [MarshalAs(UnmanagedType.LPWStr)] public string ShareName; public DFS_TARGET_PRIORITY TargetPriority; } public struct DFS_TARGET_PRIORITY { public DFS_TARGET_PRIORITY_CLASS TargetPriorityClass; public UInt16 TargetPriorityRank; public UInt16 Reserved; } public enum DFS_TARGET_PRIORITY_CLASS { DfsInvalidPriorityClass = -1, DfsSiteCostNormalPriorityClass = 0, DfsGlobalHighPriorityClass = 1, DfsSiteCostHighPriorityClass = 2, DfsSiteCostLowPriorityClass = 3, DfsGlobalLowPriorityClass = 4 } public enum DFS_STORAGE_STATE { DFS_STORAGE_STATE_OFFLINE = 1, DFS_STORAGE_STATE_ONLINE = 2, DFS_STORAGE_STATE_ACTIVE = 4, DFS_STORAGE_STATES = 0xF, } public static List<PSObject> NetDfsEnum(string DfsName) { IntPtr buffer = new IntPtr(); int EntriesRead = 0; int ResumeHere = 0; List<PSObject> returnList = new List<PSObject>(); const int MAX_PREFERRED_LENGTH = 0xFFFFFFF; const int NERR_Success = 0; try { int result = NetDfsEnum(DfsName, 3, MAX_PREFERRED_LENGTH, out buffer, out EntriesRead, ref ResumeHere); if (result != NERR_Success) { string errorMessage = new Win32Exception(Marshal.GetLastWin32Error()).Message; throw (new SystemException("NetDfsEnum error. System Error Code: " + result + " - " + errorMessage)); } else { for (int n = 0; n < EntriesRead; n++) { IntPtr DfsPtr = new IntPtr(buffer.ToInt64() + n * Marshal.SizeOf(typeof(DFS_INFO_3))); object dfsObject = Marshal.PtrToStructure(DfsPtr, typeof(DFS_INFO_3)); DFS_INFO_3 dfsInfo = (DFS_INFO_3)dfsObject; for (int i = 0; i < dfsInfo.NumberOfStorages; i++) { IntPtr storage = new IntPtr(dfsInfo.Storages.ToInt64() + i * Marshal.SizeOf(typeof(DFS_STORAGE_INFO))); DFS_STORAGE_INFO storageInfo = (DFS_STORAGE_INFO)Marshal.PtrToStructure(storage, typeof(DFS_STORAGE_INFO)); PSObject psObject = new PSObject(); psObject.Properties.Add(new PSNoteProperty("FullOriginalQueryPath", DfsName)); psObject.Properties.Add(new PSNoteProperty("DfsEntryPath", dfsInfo.EntryPath)); psObject.Properties.Add(new PSNoteProperty("DfsTarget", System.IO.Path.Combine(new string[] { @"\\", storageInfo.ServerName, storageInfo.ShareName }))); psObject.Properties.Add(new PSNoteProperty("DfsTargetState", storageInfo.State)); psObject.Properties.Add(new PSNoteProperty("TargetServerName", storageInfo.ServerName)); psObject.Properties.Add(new PSNoteProperty("TargetShareName", storageInfo.ShareName)); returnList.Add(psObject); } } } } finally { NetApiBufferFree(buffer); } return returnList; } public static List<PSObject> NetDfsEnum6(string DfsName) { IntPtr buffer = new IntPtr(); int EntriesRead = 0; int ResumeHere = 0; List<PSObject> returnList = new List<PSObject>(); const int MAX_PREFERRED_LENGTH = 0xFFFFFFF; const int NERR_Success = 0; const int Level = 6; try { int result = NetDfsEnum(DfsName, Level, MAX_PREFERRED_LENGTH, out buffer, out EntriesRead, ref ResumeHere); if (result != NERR_Success) { string errorMessage = new Win32Exception(Marshal.GetLastWin32Error()).Message; string customErrorMessage = "NetDfsEnum error for '" + DfsName + "'. System Error Code: " + result + " - " + errorMessage; throw (new SystemException(customErrorMessage)); } else { Int64 dfsStart = buffer.ToInt64(); Type dfsType = typeof(DFS_INFO_6); Int64 dfsSize = Marshal.SizeOf(dfsType); for (int n = 0; n < EntriesRead; n++) { IntPtr dfsPtr = new IntPtr(dfsStart + n * dfsSize); object dfsObject = Marshal.PtrToStructure(dfsPtr, dfsType); DFS_INFO_6 dfsInfo = (DFS_INFO_6)dfsObject; //if (dfsInfo.EntryPath == DfsName) { // skip link for namespace // continue; //} Int64 storagesStart = dfsInfo.Storages.ToInt64(); Type storageType = typeof(DFS_STORAGE_INFO_1); Int64 storageSize = Marshal.SizeOf(storageType); for (int i = 0; i < dfsInfo.NumberOfStorages; i++) { //Attempted some different properties in case they were mis-mapped the same way that NumberofStorages was //Int64 StartPoint = Convert.ToInt64(dfsInfo.MetadataSize); //System.AccessViolationException //Int64 StartPoint = Convert.ToInt64(dfsInfo.PropertyFlags); //System.AccessViolationException //Int64 StartPoint = Convert.ToInt64(dfsInfo.Timeout); //System.AccessViolationException //IntPtr storagePtr = new IntPtr(StartPoint); IntPtr storagePtr = new IntPtr(storagesStart + i * storageSize); object storageObject = Marshal.PtrToStructure(storagePtr, storageType); //System.NullReferenceException DFS_STORAGE_INFO_1 storageInfo = (DFS_STORAGE_INFO_1)storageObject; PSObject psObject = new PSObject(); psObject.Properties.Add(new PSNoteProperty("FullOriginalQueryPath", DfsName)); psObject.Properties.Add(new PSNoteProperty("DfsEntryPath", dfsInfo.EntryPath)); psObject.Properties.Add(new PSNoteProperty("DfsTarget", System.IO.Path.Combine(new string[] { @"", storageInfo.ServerName, storageInfo.ShareName }))); psObject.Properties.Add(new PSNoteProperty("DfsTargetState", storageInfo.State)); psObject.Properties.Add(new PSNoteProperty("TargetServerName", storageInfo.ServerName)); psObject.Properties.Add(new PSNoteProperty("TargetShareName", storageInfo.ShareName)); returnList.Add(psObject); } } } } finally { NetApiBufferFree(buffer); } return returnList; } public static List<PSObject> NetDfsGetInfo(string DfsEntryPath) { IntPtr buffer = new IntPtr(); List<PSObject> returnList = new List<PSObject>(); try { int result = NetDfsGetInfo(DfsEntryPath, null, null, 3, ref buffer); if (result != 0) { throw (new SystemException("Error getting DFS information")); } else { DFS_INFO_3 dfsInfo = (DFS_INFO_3)Marshal.PtrToStructure(buffer, typeof(DFS_INFO_3)); for (int i = 0; i < dfsInfo.NumberOfStorages; i++) { IntPtr storage = new IntPtr(dfsInfo.Storages.ToInt64() + i * Marshal.SizeOf(typeof(DFS_STORAGE_INFO))); DFS_STORAGE_INFO storageInfo = (DFS_STORAGE_INFO)Marshal.PtrToStructure(storage, typeof(DFS_STORAGE_INFO)); PSObject psObject = new PSObject(); psObject.Properties.Add(new PSNoteProperty("State", storageInfo.State)); psObject.Properties.Add(new PSNoteProperty("ServerName", storageInfo.ServerName)); psObject.Properties.Add(new PSNoteProperty("ShareName", storageInfo.ShareName)); returnList.Add(psObject); } } } finally { NetApiBufferFree(buffer); } return returnList; } public static List<PSObject> NetDfsGetClientInfo(string DfsPath) { IntPtr buffer = new IntPtr(); List<PSObject> returnList = new List<PSObject>(); try { int result = NetDfsGetClientInfo(DfsPath, null, null, 3, ref buffer); if (result != 0) { throw (new SystemException("Error getting DFS information")); } else { DFS_INFO_3 dfsInfo = (DFS_INFO_3)Marshal.PtrToStructure(buffer, typeof(DFS_INFO_3)); for (int i = 0; i < dfsInfo.NumberOfStorages; i++) { IntPtr storage = new IntPtr(dfsInfo.Storages.ToInt64() + i * Marshal.SizeOf(typeof(DFS_STORAGE_INFO))); DFS_STORAGE_INFO storageInfo = (DFS_STORAGE_INFO)Marshal.PtrToStructure(storage, typeof(DFS_STORAGE_INFO)); PSObject psObject = new PSObject(); psObject.Properties.Add(new PSNoteProperty("State", storageInfo.State)); psObject.Properties.Add(new PSNoteProperty("ServerName", storageInfo.ServerName)); psObject.Properties.Add(new PSNoteProperty("ShareName", storageInfo.ShareName)); returnList.Add(psObject); } } } finally { NetApiBufferFree(buffer); } return returnList; } } "@ # Definition of Module 'PsBootstrapCss' is below function ConvertTo-HtmlList { Param ( # Array of strings to convert to an HTML unordered list [parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true)] [ValidateNotNullOrEmpty()] [string[]]$InputObject ) begin { $UL = @() $UL += '<ul>' } Process { foreach ($ThisObject in $InputObject) { $UL += "<li>$ThisObject</li>"} } end { $UL += '</ul>' Write-Output $UL } } function Get-BootstrapTemplate { @" <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head> <style type="text/css"> /*! * Bootstrap v5.1.3 (https://getbootstrap.com/) * Copyright 2011-2021 The Bootstrap Authors * Copyright 2011-2021 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ :root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-color-rgb:33,37,41;--bs-body-bg-rgb:255,255,255;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#6c757d}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:var(--bs-gutter-x,.75rem);padding-left:var(--bs-gutter-x,.75rem);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-bg:transparent;--bs-table-accent-bg:transparent;--bs-table-striped-color:#212529;--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:#212529;--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:#212529;--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:#212529;vertical-align:top;border-color:#dee2e6}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table>:not(:first-child){border-top:2px solid currentColor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover>*{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-bg:#cfe2ff;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:#000;border-color:#bacbe6}.table-secondary{--bs-table-bg:#e2e3e5;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:#000;border-color:#cbccce}.table-success{--bs-table-bg:#d1e7dd;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:#000;border-color:#bcd0c7}.table-info{--bs-table-bg:#cff4fc;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:#000;border-color:#badce3}.table-warning{--bs-table-bg:#fff3cd;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:#000;border-color:#e6dbb9}.table-danger{--bs-table-bg:#f8d7da;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:#000;border-color:#dfc2c4}.table-light{--bs-table-bg:#f8f9fa;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:#000;border-color:#dfe0e1}.table-dark{--bs-table-bg:#212529;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:#fff;border-color:#373b3e}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#6c757d}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + .5rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 1rem + 2px)}.form-control-color{width:3rem;height:auto;padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em;border-radius:.25rem}.form-control-color::-webkit-color-swatch{height:1.5em;border-radius:.25rem}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(0.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:.2rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:.3rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;height:100%;padding:1rem .75rem;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control{padding:1rem .75rem}.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#198754}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(25,135,84,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#198754;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#198754}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#198754}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#198754}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#198754}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group .form-control.is-valid,.input-group .form-select.is-valid,.was-validated .input-group .form-control:valid,.was-validated .input-group .form-select:valid{z-index:1}.input-group .form-control.is-valid:focus,.input-group .form-select.is-valid:focus,.was-validated .input-group .form-control:valid:focus,.was-validated .input-group .form-select:valid:focus{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#dc3545}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#dc3545}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#dc3545}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group .form-control.is-invalid,.input-group .form-select.is-invalid,.was-validated .input-group .form-control:invalid,.was-validated .input-group .form-select:invalid{z-index:2}.input-group .form-control.is-invalid:focus,.input-group .form-select.is-invalid:focus,.was-validated .input-group .form-control:invalid:focus,.was-validated .input-group .form-select:invalid:focus{z-index:3}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#212529;text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529}.btn-check:focus+.btn,.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{pointer-events:none;opacity:.65}.btn-primary{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-primary:hover{color:#fff;background-color:#0b5ed7;border-color:#0a58ca}.btn-check:focus+.btn-primary,.btn-primary:focus{color:#fff;background-color:#0b5ed7;border-color:#0a58ca;box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-check:active+.btn-primary,.btn-check:checked+.btn-primary,.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0a58ca;border-color:#0a53be}.btn-check:active+.btn-primary:focus,.btn-check:checked+.btn-primary:focus,.btn-primary.active:focus,.btn-primary:active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5c636a;border-color:#565e64}.btn-check:focus+.btn-secondary,.btn-secondary:focus{color:#fff;background-color:#5c636a;border-color:#565e64;box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-check:active+.btn-secondary,.btn-check:checked+.btn-secondary,.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#565e64;border-color:#51585e}.btn-check:active+.btn-secondary:focus,.btn-check:checked+.btn-secondary:focus,.btn-secondary.active:focus,.btn-secondary:active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-success{color:#fff;background-color:#198754;border-color:#198754}.btn-success:hover{color:#fff;background-color:#157347;border-color:#146c43}.btn-check:focus+.btn-success,.btn-success:focus{color:#fff;background-color:#157347;border-color:#146c43;box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-check:active+.btn-success,.btn-check:checked+.btn-success,.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#146c43;border-color:#13653f}.btn-check:active+.btn-success:focus,.btn-check:checked+.btn-success:focus,.btn-success.active:focus,.btn-success:active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#198754;border-color:#198754}.btn-info{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-info:hover{color:#000;background-color:#31d2f2;border-color:#25cff2}.btn-check:focus+.btn-info,.btn-info:focus{color:#000;background-color:#31d2f2;border-color:#25cff2;box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-check:active+.btn-info,.btn-check:checked+.btn-info,.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#000;background-color:#3dd5f3;border-color:#25cff2}.btn-check:active+.btn-info:focus,.btn-check:checked+.btn-info:focus,.btn-info.active:focus,.btn-info:active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-info.disabled,.btn-info:disabled{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-warning{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#000;background-color:#ffca2c;border-color:#ffc720}.btn-check:focus+.btn-warning,.btn-warning:focus{color:#000;background-color:#ffca2c;border-color:#ffc720;box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-check:active+.btn-warning,.btn-check:checked+.btn-warning,.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#000;background-color:#ffcd39;border-color:#ffc720}.btn-check:active+.btn-warning:focus,.btn-check:checked+.btn-warning:focus,.btn-warning.active:focus,.btn-warning:active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#bb2d3b;border-color:#b02a37}.btn-check:focus+.btn-danger,.btn-danger:focus{color:#fff;background-color:#bb2d3b;border-color:#b02a37;box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-check:active+.btn-danger,.btn-check:checked+.btn-danger,.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#b02a37;border-color:#a52834}.btn-check:active+.btn-danger:focus,.btn-check:checked+.btn-danger:focus,.btn-danger.active:focus,.btn-danger:active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-light{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:focus+.btn-light,.btn-light:focus{color:#000;background-color:#f9fafb;border-color:#f9fafb;box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-check:active+.btn-light,.btn-check:checked+.btn-light,.btn-light.active,.btn-light:active,.show>.btn-light.dropdown-toggle{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:active+.btn-light:focus,.btn-check:checked+.btn-light:focus,.btn-light.active:focus,.btn-light:active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-light.disabled,.btn-light:disabled{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-dark{color:#fff;background-color:#212529;border-color:#212529}.btn-dark:hover{color:#fff;background-color:#1c1f23;border-color:#1a1e21}.btn-check:focus+.btn-dark,.btn-dark:focus{color:#fff;background-color:#1c1f23;border-color:#1a1e21;box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-check:active+.btn-dark,.btn-check:checked+.btn-dark,.btn-dark.active,.btn-dark:active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1a1e21;border-color:#191c1f}.btn-check:active+.btn-dark:focus,.btn-check:checked+.btn-dark:focus,.btn-dark.active:focus,.btn-dark:active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#212529;border-color:#212529}.btn-outline-primary{color:#0d6efd;border-color:#0d6efd}.btn-outline-primary:hover{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-check:active+.btn-outline-primary,.btn-check:checked+.btn-outline-primary,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show,.btn-outline-primary:active{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:active+.btn-outline-primary:focus,.btn-check:checked+.btn-outline-primary:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus,.btn-outline-primary:active:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0d6efd;background-color:transparent}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-check:active+.btn-outline-secondary,.btn-check:checked+.btn-outline-secondary,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show,.btn-outline-secondary:active{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:active+.btn-outline-secondary:focus,.btn-check:checked+.btn-outline-secondary:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus,.btn-outline-secondary:active:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-success{color:#198754;border-color:#198754}.btn-outline-success:hover{color:#fff;background-color:#198754;border-color:#198754}.btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-check:active+.btn-outline-success,.btn-check:checked+.btn-outline-success,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show,.btn-outline-success:active{color:#fff;background-color:#198754;border-color:#198754}.btn-check:active+.btn-outline-success:focus,.btn-check:checked+.btn-outline-success:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus,.btn-outline-success:active:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#198754;background-color:transparent}.btn-outline-info{color:#0dcaf0;border-color:#0dcaf0}.btn-outline-info:hover{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-check:active+.btn-outline-info,.btn-check:checked+.btn-outline-info,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show,.btn-outline-info:active{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:active+.btn-outline-info:focus,.btn-check:checked+.btn-outline-info:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus,.btn-outline-info:active:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#0dcaf0;background-color:transparent}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-check:active+.btn-outline-warning,.btn-check:checked+.btn-outline-warning,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show,.btn-outline-warning:active{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:active+.btn-outline-warning:focus,.btn-check:checked+.btn-outline-warning:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus,.btn-outline-warning:active:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-check:active+.btn-outline-danger,.btn-check:checked+.btn-outline-danger,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show,.btn-outline-danger:active{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:active+.btn-outline-danger:focus,.btn-check:checked+.btn-outline-danger:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus,.btn-outline-danger:active:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-check:active+.btn-outline-light,.btn-check:checked+.btn-outline-light,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show,.btn-outline-light:active{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:active+.btn-outline-light:focus,.btn-check:checked+.btn-outline-light:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus,.btn-outline-light:active:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-dark{color:#212529;border-color:#212529}.btn-outline-dark:hover{color:#fff;background-color:#212529;border-color:#212529}.btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-check:active+.btn-outline-dark,.btn-check:checked+.btn-outline-dark,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show,.btn-outline-dark:active{color:#fff;background-color:#212529;border-color:#212529}.btn-check:active+.btn-outline-dark:focus,.btn-check:checked+.btn-outline-dark:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus,.btn-outline-dark:active:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#212529;background-color:transparent}.btn-link{font-weight:400;color:#0d6efd;text-decoration:underline}.btn-link:hover{color:#0a58ca}.btn-link.disabled,.btn-link:disabled{color:#6c757d}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropend,.dropstart,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;z-index:1000;display:none;min-width:10rem;padding:.5rem 0;margin:0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:.125rem}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid rgba(0,0,0,.15)}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#212529;text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1e2125;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0d6efd}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#212529}.dropdown-menu-dark{color:#dee2e6;background-color:#343a40;border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item{color:#dee2e6}.dropdown-menu-dark .dropdown-item:focus,.dropdown-menu-dark .dropdown-item:hover{color:#fff;background-color:rgba(255,255,255,.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:#fff;background-color:#0d6efd}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:#adb5bd}.dropdown-menu-dark .dropdown-divider{border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item-text{color:#dee2e6}.dropdown-menu-dark .dropdown-header{color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;color:#0d6efd;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:#0a58ca}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;background:0 0;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6;isolation:isolate}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{background:0 0;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#0d6efd}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding-top:.5rem;padding-bottom:.5rem}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;text-decoration:none;white-space:nowrap}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem;transition:box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 .25rem}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas-header{display:none}.navbar-expand-sm .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-sm .offcanvas-bottom,.navbar-expand-sm .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas-header{display:none}.navbar-expand-md .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-md .offcanvas-bottom,.navbar-expand-md .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas-header{display:none}.navbar-expand-lg .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-lg .offcanvas-bottom,.navbar-expand-lg .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas-header{display:none}.navbar-expand-xl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xl .offcanvas-bottom,.navbar-expand-xl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xxl .offcanvas-bottom,.navbar-expand-xxl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas-header{display:none}.navbar-expand .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand .offcanvas-bottom,.navbar-expand .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.55)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.55);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.55)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.55)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.55);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.55)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:1rem 1rem}.card-title{margin-bottom:.5rem}.card-subtitle{margin-top:-.25rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:1rem}.card-header{padding:.5rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.5rem 1rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.5rem;margin-bottom:-.5rem;margin-left:-.5rem;border-bottom:0}.card-header-pills{margin-right:-.5rem;margin-left:-.5rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-group>.card{margin-bottom:.75rem}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:1rem 1.25rem;font-size:1rem;color:#212529;text-align:left;background-color:#fff;border:0;border-radius:0;overflow-anchor:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,border-radius .15s ease}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:#0c63e4;background-color:#e7f1ff;box-shadow:inset 0 -1px 0 rgba(0,0,0,.125)}.accordion-button:not(.collapsed)::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");transform:rotate(-180deg)}.accordion-button::after{flex-shrink:0;width:1.25rem;height:1.25rem;margin-left:auto;content:"";background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-size:1.25rem;transition:transform .2s ease-in-out}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.accordion-header{margin-bottom:0}.accordion-item{background-color:#fff;border:1px solid rgba(0,0,0,.125)}.accordion-item:first-of-type{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.accordion-item:first-of-type .accordion-button{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-body{padding:1rem 1.25rem}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button{border-radius:0}.breadcrumb{display:flex;flex-wrap:wrap;padding:0 0;margin-bottom:1rem;list-style:none}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;color:#0d6efd;text-decoration:none;background-color:#fff;border:1px solid #dee2e6;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:#0a58ca;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;color:#0a58ca;background-color:#e9ecef;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item.active .page-link{z-index:3;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:#dee2e6}.page-link{padding:.375rem .75rem}.page-item:first-child .page-link{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{position:relative;padding:1rem 1rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{color:#084298;background-color:#cfe2ff;border-color:#b6d4fe}.alert-primary .alert-link{color:#06357a}.alert-secondary{color:#41464b;background-color:#e2e3e5;border-color:#d3d6d8}.alert-secondary .alert-link{color:#34383c}.alert-success{color:#0f5132;background-color:#d1e7dd;border-color:#badbcc}.alert-success .alert-link{color:#0c4128}.alert-info{color:#055160;background-color:#cff4fc;border-color:#b6effb}.alert-info .alert-link{color:#04414d}.alert-warning{color:#664d03;background-color:#fff3cd;border-color:#ffecb5}.alert-warning .alert-link{color:#523e02}.alert-danger{color:#842029;background-color:#f8d7da;border-color:#f5c2c7}.alert-danger .alert-link{color:#6a1a21}.alert-light{color:#636464;background-color:#fefefe;border-color:#fdfdfe}.alert-light .alert-link{color:#4f5050}.alert-dark{color:#141619;background-color:#d3d3d4;border-color:#bcbebf}.alert-dark .alert-link{color:#101214}@-webkit-keyframes progress-bar-stripes{0%{background-position-x:1rem}}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#0d6efd;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>li::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.5rem 1rem;color:#212529;text-decoration:none;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#084298;background-color:#cfe2ff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#084298;background-color:#bacbe6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#084298;border-color:#084298}.list-group-item-secondary{color:#41464b;background-color:#e2e3e5}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#41464b;background-color:#cbccce}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#41464b;border-color:#41464b}.list-group-item-success{color:#0f5132;background-color:#d1e7dd}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#0f5132;background-color:#bcd0c7}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#0f5132;border-color:#0f5132}.list-group-item-info{color:#055160;background-color:#cff4fc}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#055160;background-color:#badce3}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#055160;border-color:#055160}.list-group-item-warning{color:#664d03;background-color:#fff3cd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#664d03;background-color:#e6dbb9}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#664d03;border-color:#664d03}.list-group-item-danger{color:#842029;background-color:#f8d7da}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#842029;background-color:#dfc2c4}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#842029;border-color:#842029}.list-group-item-light{color:#636464;background-color:#fefefe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636464;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636464;border-color:#636464}.list-group-item-dark{color:#141619;background-color:#d3d3d4}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#141619;background-color:#bebebf}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#141619;border-color:#141619}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;border-radius:.25rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{width:350px;max-width:100%;font-size:.875rem;pointer-events:auto;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .5rem 1rem rgba(0,0,0,.15);border-radius:.25rem}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:.75rem}.toast-header{display:flex;align-items:center;padding:.5rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-header .btn-close{margin-right:-.375rem;margin-left:.75rem}.toast-body{padding:.75rem;word-wrap:break-word}.modal{position:fixed;top:0;left:0;z-index:1055;display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1050;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .btn-close{padding:.5rem .5rem;margin:-.5rem -.5rem -.5rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;flex-shrink:0;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{height:calc(100% - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}.modal-fullscreen .modal-footer{border-radius:0}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}.modal-fullscreen-sm-down .modal-footer{border-radius:0}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}.modal-fullscreen-md-down .modal-footer{border-radius:0}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}.modal-fullscreen-lg-down .modal-footer{border-radius:0}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}.modal-fullscreen-xl-down .modal-footer{border-radius:0}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}.modal-fullscreen-xxl-down .modal-footer{border-radius:0}}.tooltip{position:absolute;z-index:1080;display:block;margin:0;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .tooltip-arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[data-popper-placement^=right],.bs-tooltip-end{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[data-popper-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[data-popper-placement^=left],.bs-tooltip-start{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1070;display:block;max-width:276px;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .popover-arrow{position:absolute;display:block;width:1rem;height:.5rem}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f0f0f0}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem 1rem;margin-bottom:0;font-size:1rem;background-color:#f0f0f0;border-bottom:1px solid rgba(0,0,0,.2);border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:1rem 1rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}@-webkit-keyframes spinner-border{to{transform:rotate(360deg)}}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.offcanvas{position:fixed;bottom:0;z-index:1045;display:flex;flex-direction:column;max-width:100%;visibility:hidden;background-color:#fff;background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1rem}.offcanvas-header .btn-close{padding:.5rem .5rem;margin-top:-.5rem;margin-right:-.5rem;margin-bottom:-.5rem}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;padding:1rem 1rem;overflow-y:auto}.offcanvas-start{top:0;left:0;width:400px;border-right:1px solid rgba(0,0,0,.2);transform:translateX(-100%)}.offcanvas-end{top:0;right:0;width:400px;border-left:1px solid rgba(0,0,0,.2);transform:translateX(100%)}.offcanvas-top{top:0;right:0;left:0;height:30vh;max-height:100%;border-bottom:1px solid rgba(0,0,0,.2);transform:translateY(-100%)}.offcanvas-bottom{right:0;left:0;height:30vh;max-height:100%;border-top:1px solid rgba(0,0,0,.2);transform:translateY(100%)}.offcanvas.show{transform:none}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentColor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{-webkit-animation:placeholder-glow 2s ease-in-out infinite;animation:placeholder-glow 2s ease-in-out infinite}@-webkit-keyframes placeholder-glow{50%{opacity:.2}}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;-webkit-animation:placeholder-wave 2s linear infinite;animation:placeholder-wave 2s linear infinite}@-webkit-keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.link-primary{color:#0d6efd}.link-primary:focus,.link-primary:hover{color:#0a58ca}.link-secondary{color:#6c757d}.link-secondary:focus,.link-secondary:hover{color:#565e64}.link-success{color:#198754}.link-success:focus,.link-success:hover{color:#146c43}.link-info{color:#0dcaf0}.link-info:focus,.link-info:hover{color:#3dd5f3}.link-warning{color:#ffc107}.link-warning:focus,.link-warning:hover{color:#ffcd39}.link-danger{color:#dc3545}.link-danger:focus,.link-danger:hover{color:#b02a37}.link-light{color:#f8f9fa}.link-light:focus,.link-light:hover{color:#f9fafb}.link-dark{color:#212529}.link-dark:focus,.link-dark:hover{color:#1a1e21}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:1px;min-height:1em;background-color:currentColor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:1px solid #dee2e6!important}.border-0{border:0!important}.border-top{border-top:1px solid #dee2e6!important}.border-top-0{border-top:0!important}.border-end{border-right:1px solid #dee2e6!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:1px solid #dee2e6!important}.border-start-0{border-left:0!important}.border-primary{border-color:#0d6efd!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#198754!important}.border-info{border-color:#0dcaf0!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#212529!important}.border-white{border-color:#fff!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:#6c757d!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:.25rem!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:.2rem!important}.rounded-2{border-radius:.25rem!important}.rounded-3{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-end{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-start{border-bottom-left-radius:.25rem!important;border-top-left-radius:.25rem!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} /*# sourceMappingURL=bootstrap.min.css.map */ </style><title>_ReportTitle_</title></head> <body><div class="container theme-showcase" role="main"><div class="p-5 mb-4 bg-light rounded-3 d-print-block"><h2 class="display-5 fw-bold d-print-block">_ReportTitle_</h2><p>_ReportDescription_</p></div>_ReportBody_</div></body></html> "@ } function New-BootstrapAlert { <# .SYNOPSIS Creates a new HTML div that uses the Bootstrap alert class .DESCRIPTION Creates a new HTML div that uses the Bootstrap alert class .OUTPUTS A string wih the HTML code for the div .EXAMPLE New-BootstrapAlert -Text 'blah' This example returns the following string: '<div class="alert alert-info"><strong>Info!</strong> blah</div>' #> [CmdletBinding()] param( #The HTML element to apply the Bootstrap column to [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [string[]]$Text, [Parameter( Position = 1 )] [string]$Class = 'Info' ) begin{} process{ ForEach ($String in $Text) { #"<div class=`"alert alert-$($Class.ToLower())`"><strong>$Class!</strong> $String</div>" "<div class=`"alert alert-$($Class.ToLower())`">$String</div>" } } end{} } function New-BootstrapColumn { <# .SYNOPSIS Wraps HTML elements in a Bootstrap column of the specified width .DESCRIPTION Creates a Bootstrap container which contains a row which contains a column of the specified width .OUTPUTS A string wih the code for the Bootstrap container .EXAMPLE New-BootstrapColumn -Html '<h1>Heading</h1>' This example returns the following string: '<div class="container"><div class="row justify-content-md-center"><div class="col col-lg-12"><h1>Heading</h1></div></div></div>' #> [CmdletBinding()] param( #The HTML element to apply the Bootstrap column to [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [System.String[]]$Html, [Parameter( Position = 1 )] [Int]$Width = 12 ) begin{} process{ ForEach ($OldHtml in $Html) { [String]$NewHtml = "<div class=`"container`"><div class=`"row justify-content-md-center`"><div class=`"col col-lg-$Width`">$OldHtml</div></div></div>" Write-Output $NewHtml } } end{} } function New-BootstrapDiv { <# .SYNOPSIS Creates a new HTML div that uses the Bootstrap alert class .DESCRIPTION Creates a new HTML div that uses the Bootstrap alert class .OUTPUTS A string wih the HTML code for the div .EXAMPLE New-BootstrapAlert -Text 'blah' This example returns the following string: '<div class="alert alert-info"><strong>Info!</strong> blah</div>' #> [CmdletBinding()] param( #The HTML element to apply the Bootstrap column to [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [string[]]$Text, [Parameter( Position = 1 )] [string]$Class = 'h-100 p-1 bg-light border rounded-3' ) begin{} process{ ForEach ($String in $Text) { #"<div class=`"alert alert-$($Class.ToLower())`"><strong>$Class!</strong> $String</div>" "<div class=`"alert alert-$($Class.ToLower())`">$String</div>" } } end{} } function New-BootstrapGrid { <# .SYNOPSIS Wraps HTML elements in a Bootstrap column of the specified width .DESCRIPTION Creates a Bootstrap container which contains a row which contains a column of the specified width .OUTPUTS A string wih the code for the Bootstrap container .EXAMPLE New-BootstrapColumn -Html '<h1>Heading</h1>' This example returns the following string: '<div class="container"><div class="row justify-content-md-center"><div class="col col-lg-12"><h1>Heading</h1></div></div></div>' #> [CmdletBinding()] param( #The HTML element to apply the Bootstrap column to [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [System.String[]]$Html, [string]$Justify = 'Center' ) begin{ $String = @() [decimal]$ExactWidth = 12 / ($Html | Measure-Object).Count [int]$Width = [Math]::Floor($ExactWidth) $String += "<div class=`"container`"><div class=`"row justify-content-md-$($Justify.ToLower())`">" } process{ ForEach ($OldHtml in $Html) { $String += "<div class=`"col col-lg-$Width`">$OldHtml</div>" } } end{ $String += "</div></div>" $String -join '' } } Function New-BootstrapList { <# .SYNOPSIS Upgrade a boring HTML unordered list to a fancy Bootstrap list group .DESCRIPTION Applies the Bootstrap 'table table-striped' class to an HTML table .OUTPUTS A string wih the code for the Bootstrap table .EXAMPLE New-BootstrapTable -HtmlTable '<table><tr><th>Name</th><th>Id</th></tr><tr><td>ALMon</td><td>5540</td></tr></table>' This example returns the following string: '<table class="table table-striped"><tr><th>Name</th><th>Id</th></tr><tr><td>ALMon</td><td>5540</td></tr></table>' .NOTES Author: Jeremy La Camera Last Updated: 11/6/2016 #> [CmdletBinding()] param( #The HTML table to apply the Bootstrap striped table CSS class to [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [System.String[]]$HtmlTable ) begin{} process{ ForEach ($Table in $HtmlTable) { [String]$NewTable = $Table -replace '<table>','<table class="table table-striped">' Write-Output $NewTable } } end{} } function New-BootstrapPanel { <# .SYNOPSIS Wraps HTML elements in a Bootstrap column of the specified width .DESCRIPTION Creates a Bootstrap container which contains a row which contains a column of the specified width .OUTPUTS A string wih the code for the Bootstrap container .EXAMPLE New-BootstrapColumn -Html '<h1>Heading</h1>' This example returns the following string: '<div class="container"><div class="row justify-content-md-center"><div class="col col-lg-12"><h1>Heading</h1></div></div></div>' #> [CmdletBinding()] param( #The HTML element to apply the Bootstrap column to [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [System.String[]]$Html, [string]$Class = 'default', [string]$Heading, [string]$Footer ) begin{ $String = @() $String += "<div class=`"panel panel-$($Class.ToLower())`">" if ($Heading) { $String += "<div class=`"panel-heading`">$Heading</div>" } } process{ ForEach ($OldHtml in $Html) { $String += "<div class=`"panel-body`">$OldHtml</div>" } } end{ if ($Footer) { $String += "<div class=`"panel-footer`">$Footer</div>" } $String += "</div>" $String -join '' } } function New-BootstrapReport { <# .SYNOPSIS Build a new Bootstrap report based on an HTML template .DESCRIPTION Inserts the specified title, description, and body into the HTML report template .OUTPUTS Outputs a complete HTML report as a string .EXAMPLE New-BootstrapReport -Title 'ReportTitle' -Description 'This is the report description' -Body 'This is the body of the report' .NOTES Author: Jeremy La Camera Last Updated: 11/6/2016 #> [CmdletBinding()] param( #Title of the report (displayed at the top) [String]$Title, #Description of the report (displayed below the Title) [String]$Description, #Body of the report (tables, list groups, etc.) [String[]]$Body, #The path to the HTML report template that includes the Boostrap CSS [String]$TemplatePath = "$PSScriptRoot\data\Templates\ReportTemplate.html" ) begin { if ($PSBoundParameters.ContainsKey('TemplatePath')) { [String]$Report = Get-Content $TemplatePath if ($null -eq $Report) { Write-Host "$TemplatePath not loaded. Failure. Error." } } else { [String]$Report = Get-BootstrapTemplate } } process { # Turn URLs into hyperlinks $URLs = ($Body | Select-String -Pattern 'http[s]?:\/\/[^\s\"\<\>\#\%\{\}\|\\\^\~\[\]\`]*' -AllMatches).Matches.Value | Sort-Object -Unique foreach ($URL in $URLs) { if ($URL.Length -gt 50) { $Body = $Body.Replace($URL, "<a href=$URL>$($URL[0..46] -join '')...</a>") } else { $Body = $Body.Replace($URL, "<a href=$URL>$URL</a>") } } $Report = $Report -replace '_ReportTitle_', $Title $Report = $Report -replace '_ReportDescription_', $Description $Report = $Report -replace '_ReportBody_', $Body } end { Write-Output $Report } } Function New-BootstrapTable { <# .SYNOPSIS Upgrade a boring HTML table to a fancy Bootstrap table .DESCRIPTION Applies the Bootstrap 'table table-striped' class to an HTML table .OUTPUTS A string wih the code for the Bootstrap table .EXAMPLE New-BootstrapTable -HtmlTable '<table><tr><th>Name</th><th>Id</th></tr><tr><td>ALMon</td><td>5540</td></tr></table>' This example returns the following string: '<table class="table table-striped"><tr><th>Name</th><th>Id</th></tr><tr><td>ALMon</td><td>5540</td></tr></table>' .NOTES Author: Jeremy La Camera Last Updated: 11/6/2016 #> [CmdletBinding()] param( #The HTML table to apply the Bootstrap striped table CSS class to [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [System.String[]]$HtmlTable ) begin{} process{ ForEach ($Table in $HtmlTable) { [String]$NewTable = $Table -replace '<table>','<table class="table table-striped">' Write-Output $NewTable } } end{} } function New-HtmlAnchor{ <# .SYNOPSIS Build a new HTML anchor .DESCRIPTION Inserts the specified HTML element into an HTML anchor with the specified name .OUTPUTS Outputs the heading as a string .EXAMPLE New-HtmlAnchor -Element "<h1>SampleHeader</h1>" -Name "AnchorToSampleHeader" #> [CmdletBinding()] param( #The text of the heading [Parameter( Position=0, ValueFromPipeline=$true, Mandatory=$true )] [String[]]$Element, #The heading level to generate (New-HtmlHeading can create h1, h2, h3, h4, h5, or h6 tags) [Parameter(Mandatory)] [String]$Name ) begin{} process{ Write-Output "<h$Level>$Text</h$Level>" } end{} } function New-HtmlHeading{ <# .SYNOPSIS Build a new HTML heading .DESCRIPTION Inserts the specified text into an HTML heading of the specified level .OUTPUTS Outputs the heading as a string .EXAMPLE New-HtmlHeading -Text 'Example Heading' #> [CmdletBinding()] param( #The text of the heading [Parameter( Position=0, ValueFromPipeline=$True )] [String[]]$Text, #The heading level to generate (New-HtmlHeading can create h1, h2, h3, h4, h5, or h6 tags) [ValidateRange(1,6)] [Int16]$Level = 1 ) begin{} process{ Write-Output "<h$Level>$Text</h$Level>" } end{} } function New-HtmlParagraph { <# .SYNOPSIS Build a new HTML heading .DESCRIPTION Inserts the specified text into an HTML heading of the specified level .OUTPUTS Outputs the heading as a string .EXAMPLE New-HtmlHeading -Text 'Example Heading' #> [CmdletBinding()] param( #The text of the heading [Parameter( Position=0, ValueFromPipeline=$True )] [String[]]$Text, #The heading level to generate (New-HtmlHeading can create h1, h2, h3, h4, h5, or h6 tags) [ValidateRange(1,6)] [Int16]$Level = 1 ) begin{} process{ Write-Output "<h$Level>$Text</h$Level>" } end{} } <# # Add any custom C# classes as usable (exported) types $CSharpFiles = Get-ChildItem -Path "$PSScriptRoot\*.cs" ForEach ($ThisFile in $CSharpFiles) { Add-Type -Path $ThisFile.FullName -ErrorAction Stop } #> # Definition of Module 'Permission' is below function Get-FolderAccessList { param ( $FolderTargets, $LevelsOfSubfolders ) ForEach ($ThisFolder in $FolderTargets) { $Subfolders = $null $Subfolders = Get-Subfolder -TargetPath $ThisFolder -FolderRecursionDepth $LevelsOfSubfolders -ErrorAction Continue Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExport-Permission`tGet-FolderAccessList`tFolders (including parent): $($Subfolders.Count + 1)" Get-FolderAce -LiteralPath $ThisFolder -IncludeInherited if ($Subfolders) { Split-Thread -Command Get-FolderAce -InputObject $Subfolders -InputParameter LiteralPath } } } function Get-FolderPermissionsBlock { param ( $FolderPermissions, $AccountsToSkip, $ExcludeEmptyGroups, $DomainToIgnore ) ForEach ($ThisFolder in $FolderPermissions) { $ThisHeading = New-HtmlHeading "Accounts with access to $($ThisFolder.Name)" -Level 5 $Leaf = $ThisFolder.Name | Split-Path -Parent | Split-Path -Leaf -ErrorAction SilentlyContinue if ($Leaf) { $ParentLeaf = $Leaf } else { $ParentLeaf = $ThisFolder.Name | Split-Path -Parent } if ('' -ne $ParentLeaf) { if (($ThisFolder.Group.FolderInheritanceEnabled | Select-Object -First 1) -eq $true) { $ThisSubHeading = "Accounts with access to the parent folder and subfolders ($ParentLeaf) can access this folder. So can any users listed below:" } else { $ThisSubHeading = "Accounts with access to the parent folder and subfolders ($ParentLeaf) cannot access this folder unless they are listed below:" } } else { $ThisSubHeading = "This is the top-level folder. It can only be accessed by the users listed below:" } $FilteredAccounts = $ThisFolder.Group | Group-Object -Property Account | # Skip the accounts we need to skip Where-Object -FilterScript { ![bool]$( ForEach ($RegEx in $AccountsToSkip) { if ($_.Name -match $RegEx) { $true } } ) } if ($ExcludeEmptyGroups) { $FilteredAccounts = $FilteredAccounts | Where-Object -FilterScript { #Eliminate empty groups (not useful to see in the middle of a list of users/job titles/departments/etc). $_.Group.SchemaClassName -notcontains 'Group' } } $ThisTable = $FilteredAccounts | Select-Object -Property @{Label = 'Account'; Expression = { $_.Name } }, @{Label = 'Access'; Expression = { ($_.Group | Sort-Object -Property IdentityReference -Unique).Access -join ' ; ' } }, @{Label = 'Due to Membership In'; Expression = { $GroupString = ($_.Group.IdentityReference | Sort-Object -Unique) -join ' ; ' ForEach ($IgnoreThisDomain in $DomainToIgnore) { $GroupString = $GroupString -replace $IgnoreThisDomain, '' } $GroupString } }, @{Label = 'Name'; Expression = { $_.Group.Name | Sort-Object -Unique } }, @{Label = 'Department'; Expression = { $_.Group.Department | Sort-Object -Unique } }, @{Label = 'Title'; Expression = { $_.Group.Title | Sort-Object -Unique } } | ConvertTo-Html -Fragment | New-BootstrapTable New-BootstrapDiv -Text ($ThisHeading + $ThisSubHeading + $ThisTable) } } function Get-FolderTableHeader { param ($LevelsOfSubfolders) switch ($LevelsOfSubfolders ) { 0 { 'Includes the target folder only (option to report on subfolders was declined)' } -1 { 'Includes the target folder and all subfolders with unique permissions' } default { "Includes the target folder and $LevelsOfSubfolders levels of subfolders with unique permissions" } } } function Get-HtmlBody { param ( $FolderList, $HtmlFolderPermissions ) (New-HtmlHeading "Folders with Permissions in This Report" -Level 3) + $FolderList + (New-HtmlHeading "Accounts Included in Those Permissions" -Level 3) + $HtmlFolderPermissions } function Get-HtmlFolderList { param ( $FolderTableHeader, $HtmlTableOfFolders ) New-BootstrapDiv -Text ( (New-HtmlHeading $FolderTableHeader -Level 5) + $HtmlTableOfFolders ) } function Get-PrtgXmlSensorOutput { param ( $NtfsIssues ) $Channels = [System.Collections.Generic.List[string]]::new() # Build our XML output formatted for PRTG. $ChannelParams = @{ MaxError = 0.5 Channel = 'Folders with inheritance disabled' Value = ($NtfsIssues.FoldersWithBrokenInheritance | Measure-Object).Count CustomUnit = 'folders' } Format-PrtgXmlResult @ChannelParams | ForEach-Object { $null = $Channels.Add($_) } $ChannelParams = @{ MaxError = 0.5 Channel = 'ACEs for groups breaking naming convention' Value = ($NtfsIssues.NonCompliantGroups | Measure-Object).Count CustomUnit = 'ACEs' } Format-PrtgXmlResult @ChannelParams | ForEach-Object { $null = $Channels.Add($_) } $ChannelParams = @{ MaxError = 0.5 Channel = 'ACEs for users instead of groups' Value = ($NtfsIssues.UserACEs | Measure-Object).Count CustomUnit = 'ACEs' } Format-PrtgXmlResult @ChannelParams | ForEach-Object { $null = $Channels.Add($_) } $ChannelParams = @{ MaxError = 0.5 Channel = 'ACEs for unresolvable SIDs' Value = ($NtfsIssues.SIDsToCleanup | Measure-Object).Count CustomUnit = 'ACEs' } Format-PrtgXmlResult @ChannelParams | ForEach-Object { $null = $Channels.Add($_) } $ChannelParams = @{ MaxError = 0.5 Channel = "Folders with 'CREATOR OWNER' access" Value = ($NtfsIssues.FoldersWithCreatorOwner | Measure-Object).Count CustomUnit = 'folders' } Format-PrtgXmlResult @ChannelParams | ForEach-Object { $null = $Channels.Add($_) } Format-PrtgXmlSensorOutput -PrtgXmlResult $Channels -IssueDetected:$($NtfsIssues.IssueDetected) } function Get-ReportDescription { param ($LevelsOfSubfolders) switch ($LevelsOfSubfolders ) { 0 { 'Does not include permissions on subfolders (option was declined)' } -1 { 'Includes all subfolders with unique permissions (including ∞ levels of subfolders)' } default { "Includes all subfolders with unique permissions (down to $LevelsOfSubfolders levels of subfolders)" } } } function Select-FolderTableProperty { param ( $InputObject ) $InputObject | Select-Object -Property @{ Label = 'Folder' Expression = { $_.Name } }, @{ Label = 'Inheritance' Expression = { $_.Group.FolderInheritanceEnabled | Select-Object -First 1 } } } # Add any custom C# classes as usable (exported) types $CSharpFiles = Get-ChildItem -Path "$PSScriptRoot\*.cs" ForEach ($ThisFile in $CSharpFiles) { Add-Type -Path $ThisFile.FullName -ErrorAction Stop } #----------------[ Logging ]---------------- $LogDir = New-DatedSubfolder -Root $LogDir Start-Transcript "$LogDir\Transcript.log" #----------------[ Declarations ]---------------- $DirectoryEntryCache = [hashtable]::Synchronized(@{}) $IdentityReferenceCache = [hashtable]::Synchronized(@{}) $AdsiServerCache = [hashtable]::Synchronized(@{}) $Permissions = $null $FolderTargets = $null $SecurityPrincipals = $null $AccountPermissions = $null $DedupedUserPermissions = $null $FolderPermissions = $null #----------------[ Main Execution ]--------------- Write-Verbose "$(Get-Date -Format s)`t$(hostname)`tExport-Permission`tTarget Folder: '$TargetPath'" $FolderTargets = Get-FolderTarget -FolderPath $TargetPath $ReportDescription = Get-ReportDescription -LevelsOfSubfolders $LevelsOfSubfolders $FolderTableHeader = Get-FolderTableHeader -LevelsOfSubfolders $LevelsOfSubfolders $Permissions = Get-FolderAccessList -FolderTargets $FolderTargets -LevelsOfSubfolders $LevelsOfSubfolders # If $TargetPath was on a local disk such as C:\ # The Get-FolderTarget cmdlet has replaced that local disk path with the corresponding UNC path \\$(hostname)\C$ # Unfortunately if it is the root of that local disk, Get-Item is unable to retrieve a DirectoryInfo object for the root of the share # (error: "Could not find item") # As a workaround here we will instead get the folder ACL for the original $TargetPath # But I don't think this solves it since it won't work for actual remote paths at the root of the share: \\server\share if ($null -eq $Permissions) { $Permissions = Get-FolderAccessList -FolderTargets $TargetPath -LevelsOfSubfolders $LevelsOfSubfolders } # Save a CSV of the raw NTFS ACEs, showing non-inherited ACEs only except for the root folder $TargetPath $NtfsAccessControlEntriesCsv = "$LogDir\NtfsAccessControlEntries.csv" $Permissions | Select-Object -Property @{ Label = 'Path' Expression = { $_.SourceAccessList.Path } }, IdentityReference, AccessControlType, FileSystemRights, IsInherited, PropagationFlags, InheritanceFlags | Export-Csv -NoTypeInformation -LiteralPath $NtfsAccessControlEntriesCsv Write-Information $NtfsAccessControlEntriesCsv # Resolve the Identity References directly from the NTFS ACEs to their associated SIDs/Names $ResolveAce = @{ Command = 'Resolve-Ace' InputObject = $Permissions InputParameter = 'InputObject' ObjectStringProperty = 'IdentityReference' AddParam = @{ KnownServers = $AdsiServerCache } } $Identities = Split-Thread @ResolveAce # Save a CSV report of the resolved identity references $IdentityReferencesCsv = "$LogDir\NtfsIdentityReferences.csv" $Identities | Select-Object -Property @{ Label = 'Path' Expression = { $_.SourceAccessList.Path } }, * | Export-Csv -NoTypeInformation -LiteralPath $IdentityReferencesCsv Write-Information $IdentityReferencesCsv $GroupedIdentities = $Identities | Group-Object -Property IdentityReferenceResolved # Use ADSI to collect more information about each IdentityReference (e.g. CONTOSO\user1) in NTFS Access Control Entries $ExpandIdentityReference = @{ Command = 'Expand-IdentityReference' InputObject = $GroupedIdentities InputParameter = 'AccessControlEntry' AddParam = @{ NoGroupMembers = $NoGroupMembers DirectoryEntryCache = $DirectoryEntryCache IdentityReferenceCache = $IdentityReferenceCache } ObjectStringProperty = 'Name' } $SecurityPrincipals = Split-Thread @ExpandIdentityReference # Format Security Principals (distinguish group members from users directly listed in the NTFS DACLs) # Filter out groups (their members have already been retrieved) $FormatSecurityPrincipal = @{ Command = 'Format-SecurityPrincipal' InputObject = $SecurityPrincipals InputParameter = 'SecurityPrincipal' Timeout = 1200 ObjectStringProperty = 'Name' } $AccountPermissions = Split-Thread @FormatSecurityPrincipal $ExpandedAccountPermissions = Expand-AccountPermission -AccountPermission $AccountPermissions # Save a CSV report of the expanded account permissions #TODO: Expand DirectoryEntry objects in the DirectoryEntry and Members properties $ExpandedAccountPermissionsCsv = "$LogDir\ResultantAccountPermissions.csv" $ExpandedAccountPermissions | Select-Object -Property @{ Label = 'SourceAclPath' Expression = { $_.ACESourceAccessList.Path } }, * | Export-Csv -NoTypeInformation -LiteralPath $ExpandedAccountPermissionsCsv Write-Information $ExpandedAccountPermissionsCsv $Accounts = $AccountPermissions | Group-Object -Property User | Sort-Object -Property Name # Ensure accounts only appear once on the report if they exist in multiple domains $DedupedUserPermissions = $Accounts | Remove-DuplicatesAcrossIgnoredDomains -DomainToIgnore $DomainToIgnore # Group the user permissions back into folder permissions for the report $FolderPermissions = Format-FolderPermission -UserPermission $DedupedUserPermissions | Group-Object -Property Folder | Sort-Object -Property Name $HtmlTableOfFolders = Select-FolderTableProperty -InputObject $FolderPermissions | ConvertTo-Html -Fragment | New-BootstrapTable $GetFolderPermissionsBlock = @{ FolderPermissions = $FolderPermissions AccountsToSkip = $AccountsToSkip ExcludeEmptyGroups = $ExcludeEmptyGroups DomainToIgnore = $DomainToIgnore } $HtmlFolderPermissions = Get-FolderPermissionsBlock @GetFolderPermissionsBlock ##Commented the two lines below because actually keeping semicolons means it copy/pastes better into Excel ### Convert-ToHtml will not expand in-line HTML, so we had to use semicolons as placeholders and will now replace them with line breaks. ##$HtmlFolderPermissions = $HtmlFolderPermissions -replace ' ; ','<br>' $ReportDescription = "$(New-BootstrapAlert -Class Dark -Text $TargetPath) $ReportDescription" $FolderList = Get-HtmlFolderList -FolderTableHeader $FolderTableHeader -HtmlTableOfFolders $HtmlTableOfFolders [string]$Body = Get-HtmlBody -FolderList $FolderList -HtmlFolderPermissions $HtmlFolderPermissions $ReportParameters = @{ Title = $Title Description = $ReportDescription Body = $Body } $Report = New-BootstrapReport @ReportParameters # Save the Html report $ReportFile = "$LogDir\FolderPermissionsReport.html" $Report | Set-Content -LiteralPath $ReportFile # Output the name of the report file to the Information stream Write-Information $ReportFile # Report common issues with NTFS permissions (formatted as XML for PRTG) # TODO: Users with ownership $NtfsIssueParams = @{ FolderPermissions = $FolderPermissions UserPermissions = $Accounts GroupNamingConvention = $GroupNamingConvention } $NtfsIssues = New-NtfsAclIssueReport @NtfsIssueParams # Format the information as a custom XML sensor for Paessler PRTG Network Monitor $XMLOutput = Get-PrtgXmlSensorOutput -NtfsIssues $NtfsIssues # Save the result of the custom XML sensor for Paessler PRTG Network Monitor $XmlFile = "$LogDir\PrtgSensorResult.xml" $XMLOutput | Set-Content -LiteralPath $XmlFile # Output the name of the report file to the Information stream Write-Information $XmlFile # Send the XML to a PRTG Custom XML Push sensor for tracking $PrtgSensorParams = @{ XmlOutput = $XMLOutput PrtgProbe = $PrtgProbe PrtgSensorProtocol = $PrtgSensorProtocol PrtgSensorPort = $PrtgSensorPort PrtgSensorToken = $PrtgSensorToken } Send-PrtgXmlSensorOutput @PrtgSensorParams Stop-Transcript # Output the XML so the script can be directly used as a PRTG sensor Invoke-Item $ReportFile # Output the XML so the script can be directly used as a PRTG sensor return $XMLOutput |