Approving WSUS updates for one computer group at a time - powershell

We have a WSUS server, and four computer groups (Alpha, Beta, Production, Workstations). Our patching process has us approve all "Not Approved" patches for the Alpha group, right after they're released by Microsoft. One week later, we approve all of the updates from the previous week, for the Beta group. One week later, we do the same for Production.
I'm writing a script (which I can't test until next week), and wonder if there's a better way to get the list of updates that are approved for Alpha. Here is the code:
$updateScope = New-Object Microsoft.UpdateServices.Administration.UpdateScope
$updateScope.ApprovedStates = [Microsoft.UpdateServices.Administration.ApprovedStates]::LatestRevisionApproved
$updateScope.FromArrivalDAte = (Get-Date).AddMonths(-1)
$wsusGroup = $wsus.GetComputerTargetGroups() | Where {$_.Name -eq "$PatchingGroup"}
$updateScope
$updateScope.getType()
$updateScope.count
$updateScope.ApprovedComputerTargetGroups.add($wsusGroup)
$wsus.GetUpdates($updateScope)
$Updates = $wsus.GetUpdates($updateScope)
I assume I can take the $Updates variable and do the following for the Beta and Production groups:
Foreach ($update in $updates) {
$update.Approve(“Install”,$PatchingGroup)
}
Is this going to work, and is there a better way?

Here is the code I ended-up using. It works, but I can't help feeling that there is a better way.
<#
.Synopsis
Approve WSUS updates for installation.
.DESCRIPTION
This script takes the name of a WSUS approval group, and approves updates based on their age.
.NOTES
Author: Mike Hashemi
V1 date: 24 Feb 2014
.LINK
.PARAMETER PrimaryWSUSServer
Default value: server.domain.local. This parameter specifies the DNS name of the primary WSUS server.
.PARAMETER PatchingGroup
Manadatory parameter. Valid values are 'Alpha','Beta','Production','Excluded','Workstations','COC-OMI-WORKSTATIONS'. The value of this parameter determines what patching groups will have updates approved for installation. Multiple groups can be entered at once, unless one of the is Alpha
.EXAMPLE
.\manageWSUSUpdates-Parameterized.ps1 -PatchingGroup Alpha
In this example, the script will approve all updates with an approval status not equal to 'IsDeclined', for installation to servers in the Alpha group.
.EXAMPLE
.\manageWSUSUpdates-Parameterized.ps1 -PatchingGroup Beta
In this example, the script will get the list of updates approved for the Alpha group, in the last three months (from the date the script is run), and will approve them for installation to servers in the Beta group.
#>
[CmdletBinding()]
param(
[string]$PrimaryWSUSServer = “server.domain.local”,
[Parameter(Mandatory=$True)]
[ValidateSet('Alpha','Beta','Production','Excluded','Workstations','COC-OMI-WORKSTATIONS')]
[string[]]$PatchingGroup
)
#Initialize variables
$BeginScriptTime = Get-Date
# Load the Required .NET assembly
[void][reflection.assembly]::LoadWithPartialName(“Microsoft.UpdateServices.Administration”)
$wsus = [Microsoft.UpdateServices.Administration.AdminProxy]::getUpdateServer($PrimaryWSUSServer,$False)
Function Approve-AlphaPatches {
#Get the list of all updates that are not declined.
$unapprovedUpdates = $wsus.getupdates() | where {$_.isdeclined -ne $true}
#If an update has a license agreement, accept it
$license = $unapprovedUpdates | where {$_.RequiresLicenseAgreementAcceptance}
$license | ForEach {$_.AcceptLicenseAgreement()}
#Get members of Alpha patching group.
$installGroup = $wsus.GetComputerTargetGroups() | where {$_.Name -eq $PatchingGroup}
#Approve updates for the Beta group.
Foreach ($update in $unapprovedUpdates) {
$update.Approve(“Install”,$installGroup)
}
}
Function Approve-NonAlphaPatches {
Foreach ($group in $PatchingGroup) {
#Get the updates that have arrived in the last three months.
$updateScope = New-Object Microsoft.UpdateServices.Administration.UpdateScope
$updateScope.ApprovedStates = [Microsoft.UpdateServices.Administration.ApprovedStates]::LatestRevisionApproved
$updateScope.FromArrivalDAte = (Get-Date).AddMonths(-3)
#Get the updates approved for the Alpha group.
$alphaGroup = $wsus.GetComputerTargetGroups() | Where {$_.Name -eq 'Alpha'}
$updateScope.ApprovedComputerTargetGroups.add($alphaGroup)
$Updates = $wsus.GetUpdates($updateScope)
#Get members of Alpha patching group.
$installGroup = $wsus.GetComputerTargetGroups() | where {$_.Name -eq $group}
#Approve updates for the user-specified patching group.
Foreach ($update in $updates) {
$update.Approve(“Install”,$installGroup)
}
}
}
#Begin Script
If (($PatchingGroup.Count -gt 1) -and ($PatchingGroup -ccontains 'Alpha')) {
Write-Error ("This script cannot approve Alpha patches with other patching groups. If you want to approve more groups at the same time, please approve the rest in a second execution of the script.")
Return
}
Else {
If ($PatchingGroup -eq 'Alpha') {
Approve-AlphaPatches
}
Else {
Approve-NonAlphaPatches
}
}

I couldn't see any reference to a specific version of PowerShell in your original post, but is it possible that the Windows 8.1 / Windows Server 2012 R2 WSUS module would achieve your goal?
There is a function called Approve-WsusUpdate, and it has a -TargetGroupName parameter.
http://technet.microsoft.com/en-us/library/hh826164.aspx
If you are not using Windows 8.1 and PowerShell version 4.0, then forgive my ignorance.

I added an exclude list to prevent to re-enable disabled updates:
#Load KBs to exclude
$pattern = '[^0-9]'
if(Test-Path ($PSScriptRoot + '\exclude.csv')){
$exclude = #(Import-Csv ($PSScriptRoot + '\exclude.csv') -Delimiter ';' -Encoding UTF8 | SELECT KBArticle)
}
#Approve updates for the Beta group.
Foreach ($update in $unapprovedUpdates) {
if (($exclude -eq $null) -or ($exclude | where {($_.KBArticle -replace $pattern, '') -ne $update.KnowledgebaseArticles} )){
$update.Approve(“Install”,$installGroup)
}
}
the exclude.csv is like follow:
KBArticle
KB4011052

Related

Powershell - Test if user is part of any Groups

I'm having trouble getting a small PS script to work the way I want it to.
Powershell version: 5.1
Ultimate goal: I want to parse all existing local User accounts and generate a list of those that are not part of any local Groups. (This is not an AD environment, nor part of a domain. This is all just local OS accounts and Groups.) Also, I am only interested in the Groups that I have manually created - I don't care about the default System Groups. (More on how I try to achieve this, later.) And then, after I get this part working, this output (list of User names) will then be used (in the future - not shown in this code) as input to another block that will add these not-in-a-Group users to a Group.
I found an example bit of code - which worked, and resulted in the correct set of user names. But it was slow - it took >5 minutes. So I wanted something faster.
The approach that I'm working with now generally seems like it will work, and is pretty quick. I'm just having trouble getting it to restrict things down correctly. It seems I'm having trouble referencing properties of objects returned by the cmdlets. (See code a bit further down. There are various counters and write-host steps in here, too, that are purely for helping me see what's going on and debugging - they aren't required for the actual task.)
My general outline of how I am going about this:
Compile a list of all Users in all Groups
But, I only want Groups that have no Description - these are the ones that I have created on the machine. The OS default Groups all have a Description. This will help me narrow down my list of Users.
Loop over the list of all Users on the system
Compare each user to the List from Step 1
If User is on the List, then that User is IN a Group, so skip it
If User is not on the List, then save/report that Name back
[Side note - the user 'WDAGUtilityAccount' turned out to also not be in any Groups, but I don't want to change that one; so have a specific test to exclude it from the list.]
[original version of code]
# --- Part 1
$UsersList = foreach ( $aGroup in get-localgroup ) {
$groupcount++
# Only want Groups that don't have a Description
if ( $null -eq $aGroup.Description ) {
Get-LocalGroupMember $aGroup.name
}
write-host "$groupCount -- $aGroup.Name _ $aGroup.Description"
}
# Just for debugging - to see the content of Part 1
write-host "List = $UsersList"
# ---- Part 2
Get-LocalUser | ForEach-Object {
$count++
$name = $_.Name
if ( $name -eq "WDAGUtilityAccount" ) {
# nothing - skip this one
} elseif ( $UsersList.Name -inotcontains "$env:COMPUTERNAME\$_" ) {
$count2++
write-host "$Count ($Count2) : $name -- not a part of any groups"
}
}
It appears that my attempts to extract the Properties of the Group in Part 1 are failing - I am getting literal text 'Name' and 'Description', instead of the aGroup.Name aGroup.Description property values. So my output looks like "MyGroup1.Name" instead of "MyGroup1" (assuming the actual name of the group is 'MyGroup1').
I believe that I am approaching the 'Group' object correctly - when I do 'get-localgroup | get-member" it says that it is a 'LocalGroup' object, with various properties (Name and Description being two of those).
There may be many other approaches to this - I'm interested in hearing general ideas; but I would also like to know the specific issues with my current code - it's a learning exercise. :)
Thanks, J
[ Version 2 of code - after some suggestions... ]
$UsersList = foreach ( $aGroup in get-localgroup ) {
$groupcount++
#Write-Host $aGroup.Description
# Only want Groups that don't have a Description (i.e. are the Groups that we created, not the default System/OS groups)
if ( $null -eq $($aGroup.Description) ) {
$UserList += Get-LocalGroupMember $($aGroup.name)
write-host "$groupCount -- $($aGroup.Name) _ $($aGroup.Description)"
}
}
write-host "List = $UsersList"
Get-LocalUser | ForEach-Object {
$count++
$name = $_.Name
if ( $name -eq "WDAGUtilityAccount" ) {
# nothing - skip this one
} elseif ( $($UsersList.name) -inotcontains "$env:COMPUTERNAME\$_" ) {
$count2++
write-host "$Count ($Count2) : $name -- not a part of any groups"
}
}
I believe this could be reduced to:
Store all members of a any group where the group's Description is null.
Get all local users and filter where their user's Name is not equal to WDAGUtilityAccount and they are not part of the stored member's SID array.
$members = Get-LocalGroup | Where-Object { -not $_.Description } | Get-LocalGroupMember
Get-LocalUser | Where-Object {
$_.Name -ne 'WDAGUtilityAccount' -and $_.SID -notin $members.SID
} | Format-Table -AutoSize

Powershell: Count within foreach loop

I have a Powershell script that queries AD user certificates. I'd like to check and inform the user if his VPN certificate is going to expire soon. The script runs fine so far, but I have some instances where the user already has 2 VPN certificates and I don't want to notify if this is the case (in Script on step "### Execute next steps if count is less then 2"). I've tried to add ".count" to some of the variables, but since it's within the foreach, it's always giving me a "1" as a match. I have no clue how to achieve this, please help. Here's the script:
param (
[string]$queryDN = '...DC=com',
[string]$VPNcertname = 'VPN OID',
[int]$days = 14
)
# Make decision what to check
$userpath = $queryDN
$cert2check = $VPNcertname
# Begin script
$users = (Get-ADGroupMember -Identity $userpath).distinguishedName
foreach ($dude in $users) {
$user = Get-ADUser $dude -Property Certificates
$Certificatelist = $user.Certificates | foreach {
New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $_
}
ForEach($cert in $Certificatelist){
$querycertificate = if($cert.EnhancedKeyUsageList.Where({$_.FriendlyName -eq $cert2check})){
### Execute next steps if count is less then 2
$expirationDate = $Cert.NotAfter
if ($expirationDate -lt [datetime]::Today.AddDays($days)) {
write-host The $cert2check certificate for user: $user.UserPrincipalName`, expires: $expirationDate. This is less than: $days days and should be changed soon.
}
}
}
}
Thanks
Thanks #Vesper. This was exactly what I was looking for:
$CertList2=$Certificatelist|where {$_.EnhancedKeyUsageList.FriendlyName -eq $cert2check}; if ($CertList2.count -gt 1) {...}

Missing AD module and can't get it, need something similar or something to simulate it

So I'm trying to output a complete KB list for all computers on a server (which works on one computer) but it doesn't recognize Get-ADcomputer as a cmdlet. When checking various sources, it appears that the AD module isn't included. As I'm doing this on a work computer/server I'm hesitant to download anything or anything of that nature.
Is there any way I can achieve the following without using the AD module or someway I might be missing how to import the module (if it exists, which I don't think it does on this system)?
# 1. Define credentials
$cred = Get-Credential
# 2. Define a scriptblock
$sb = {
$Session = New-Object -ComObject Microsoft.Update.Session
$Searcher = $Session.CreateUpdateSearcher()
$HistoryCount = $Searcher.GetTotalHistoryCount()
$Searcher.QueryHistory(0,$HistoryCount) | ForEach-Object -Process {
$Title = $null
if ($_.Title -match "\(KB\d{6,7}\)") {
# Split returns an array of strings
$Title = ($_.Title -split '.*\((?<KB>KB\d{6,7})\)')[1]
} else {
$Title = $_.Title
}
$Result = $null
switch ($_.ResultCode) {
0 { $Result = 'NotStarted'}
1 { $Result = 'InProgress' }
2 { $Result = 'Succeeded' }
3 { $Result = 'SucceededWithErrors' }
4 { $Result = 'Failed' }
5 { $Result = 'Aborted' }
default { $Result = $_ }
}
New-Object -TypeName PSObject -Property #{
InstalledOn = Get-Date -Date $_.Date;
Title = $Title;
Name = $_.Title;
Status = $Result
}
} | Sort-Object -Descending:$false -Property InstalledOn | Where {
$_.Title -notmatch "^Definition\sUpdate"
}
}
#Get all servers in your AD (if less than 10000)
Get-ADComputer -ResultPageSize 10000 -SearchScope Subtree -Filter {
(OperatingSystem -like "Windows*Server*")
} | ForEach-Object {
# Get the computername from the AD object
$computer = $_.Name
# Create a hash table for splatting
$HT = #{
ComputerName = $computer ;
ScriptBlock = $sb ;
Credential = $cred;
ErrorAction = "Stop";
}
# Execute the code on remote computers
try {
Invoke-Command #HT
} catch {
Write-Warning -Message "Failed to execute on $computer because $($_.Exception.Message)"
}
} | Format-Table PSComputerName,Title,Status,InstalledOn,Name -AutoSize
You've got 3 options:
First is to just install the RSAT feature for AD which will include the AD module. This is probably the best option unless there is something specific preventing it. If you're running your script from a client operating systems you need to install the RSAT first, though.
Option 2 (which should only be used if adding the Windows feature is somehow an issue) is to download and use the Quest AD tools, which give very similar functionality, but it looks like Dell is doing their best to hide these now so that may be difficult to locate...
Option 3 is to use the .NET ADSI classes to access AD directly, which will work without any additional downloads on any system capable of running PowerShell. If you'd like to go this route you should check out the documentation for the interface Here and for the System.DirectoryServices namespace Here.
Edit
Just noticed the last part of your question, what do you mean by "a complete KB list"? Not just Windows updates or things updated manually or whatever? What else would be in a list of Windows updates that was not a Windows update?
You have not mentioned the OSes you are using but in general if you have a server 2008 R2 or above, all you have to do it activate the RSAT feature AD PowerShell Module and you will have the cmdlet you are looking for.
On a client machine, you 'have to' install RSAT, and then activate the features. You can take a look at the technet article for more info: https://technet.microsoft.com/en-us/library/ee449483(v=ws.10).aspx
If you don't want to use that option, then you will have to use .NET ADSI classes. There are tons of examples on how to do this, it basically boils down to a couple of lines really. Technet has examples on this as well: https://technet.microsoft.com/en-us/library/ff730967.aspx

TFS 2015 no longer adds build number to Global List upon build complete?

In TFS 2015 new build system, did the functionality to automatically add build number to Global List (Build - Project Name) upon build complete removed?
Do I need to write a custom PowerShell task to accomplish this?
Note: XAML builds still add build number to Global List as it did before.
Since many features are still missing in the vNext build system, I've made a PowerShell script that do the Job.
In a near futur, I plan to update this script to support IntegratedIn field filling and to convert the script as a custom build task.
[CmdletBinding(SupportsShouldProcess=$false)]
param()
function Update-GlobalListXml
{
[CmdletBinding(SupportsShouldProcess=$false)]
param(
[xml]$globalListsDoc,
[parameter(Mandatory=$true)][string][ValidateNotNullOrEmpty()]$glName,
[parameter(Mandatory=$true)][string][ValidateNotNullOrEmpty()]$buildNumber
)
Write-Verbose "Checking whether '$glName' exists"
$buildList = $globalListsDoc.GLOBALLISTS.GLOBALLIST | Where-Object { $_.name -eq $glName }
if ($buildList -eq $null)
{
Write-Host "GlobalList '$glName' does not exist and will be created"
$globalLists = $globalListsDoc.GLOBALLISTS
if($globalLists.OuterXml -eq $null)
{
$newDoc = [xml]"<gl:GLOBALLISTS xmlns:gl="""http://schemas.microsoft.com/VisualStudio/2005/workitemtracking/globallists"""></gl:GLOBALLISTS>"
$globalLists = $newDoc.GLOBALLISTS
}
$globalList = $globalLists.OwnerDocument.CreateElement("GLOBALLIST")
$globalList.SetAttribute("name", $glName)
$buildList = $globalLists.AppendChild($globalList)
}
if(($buildList.LISTITEM | where-object { $_.value -eq $buildNumber }) -ne $null)
{
throw "The LISTITEM value: '$buildNumber' already exists in the GLOBALLIST: '$glName'"
}
Write-Host "Adding '$buildNumber' as a new LISTITEM in '$glName'"
$build = $buildList.OwnerDocument.CreateElement("LISTITEM")
$build.SetAttribute("value", $buildNumber)
$buildList.AppendChild($build) | out-null
return $buildList.OwnerDocument
}
function Invoke-GlobalListAPI()
{
[CmdletBinding(SupportsShouldProcess=$false)]
param(
[parameter(Mandatory=$true)][Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemStore]$wiStore,
[parameter(Mandatory=$true,ParameterSetName="Import")][switch]$import,
[parameter(Mandatory=$true,ParameterSetName="Import")][xml]$globalLists,
[parameter(ParameterSetName="Export")][switch]$export
)
try {
if($import)
{
$wiStore.ImportGlobalLists($globalLists.OuterXml) # Account must be explicitly in the Project Administrator Group
}
if($export)
{
return [xml]$wiStore.ExportGlobalLists()
}
}
catch [Microsoft.TeamFoundation.TeamFoundationServerException] {
Write-Error "An error has occured while exporting or importing GlobalList"
throw $_
}
}
function Get-WorkItemStore()
{
[CmdletBinding(SupportsShouldProcess=$false)]
param(
[parameter(Mandatory=$true)][string][ValidateNotNullOrEmpty()]$tpcUri,
[parameter(Mandatory=$true)][string][ValidateNotNullOrEmpty()]$agentWorker
)
# Loads client API binaries from agent folder
$clientDll = Join-Path $agentWorker "Microsoft.TeamFoundation.Client.dll"
$wiTDll = Join-Path $agentWorker "Microsoft.TeamFoundation.WorkItemTracking.Client.dll"
[System.Reflection.Assembly]::LoadFrom($clientDll) | Write-Verbose
[System.Reflection.Assembly]::LoadFrom($wiTDll) | Write-Verbose
try {
Write-Host "Connecting to $tpcUri"
$tfsTpc = [Microsoft.TeamFoundation.Client.TfsTeamProjectCollectionFactory]::GetTeamProjectCollection($tpcUri)
return $tfsTpc.GetService([Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemStore])
}
catch [Microsoft.TeamFoundation.TeamFoundationServerException] {
Write-Error "An error has occured while retrieving WorkItemStore"
throw $_
}
}
function Get-WITDataStore64
{
[CmdletBinding(SupportsShouldProcess=$false)]
param()
if($env:VS140COMNTOOLS -eq $null)
{
throw New-Object System.InvalidOperationException "Visual Studio 2015 must be installed on the build agent" # TODO: Change it by checking agent capabilities
}
$idePath = Join-Path (Split-Path -Parent $env:VS140COMNTOOLS) "IDE"
return Get-ChildItem -Recurse -Path $idePath -Filter "Microsoft.WITDataStore64.dll" | Select-Object -First 1 -ExpandProperty FullName
}
function Update-GlobalList
{
[CmdletBinding(SupportsShouldProcess=$false)]
param()
# Get environment variables
$tpcUri = $env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI
Write-Verbose "Team Project Collection Url: '$tpcUri'"
$teamProjectName = $env:SYSTEM_TEAMPROJECT
Write-Verbose "Team Project: '$teamProjectName'"
$buildNumber = $env:BUILD_BUILDNUMBER
Write-Verbose "Build Number: '$buildNumber'"
$agentHome = $env:AGENT_HOMEDIRECTORY
Write-Verbose "Agent home direrctory: '$agentHome'"
$globalListName = "Builds - $teamProjectName"
Write-Verbose "GlobalList name: '$teamProjectName'"
# Copy 'Microsoft.WITDataStore64.dll' from Visual Studio directory to AgentBin directory if it does not exist
$agentWorker = Join-Path $agentHome "agent\Worker"
$targetPath = Join-Path $agentWorker "Microsoft.WITDataStore64.dll" # Only compatible with x64 process #TODO use constant instead
if(-not (Test-Path $targetPath))
{
$wITDataStore64FilePath = Get-WITDataStore64
Write-Host "Copying $wITDataStore64FilePath to $targetPath"
Copy-Item $wITDataStore64FilePath $targetPath | Write-Verbose
}
$wiStore = Get-WorkItemStore -tpcUri $tpcUri -agentWorker $agentWorker
# Retrive GLOBALLISTS
$xmlDoc = Invoke-GlobalListAPI -export -wiStore $wiStore
$gls2 = Update-GlobalListXml -globalListsDoc $xmlDoc -glName $globalListName -buildNumber $buildNumber
Invoke-GlobalListAPI -import -globalLists $gls2 -wiStore $wiStore
}
Update-GlobalList
Here is the link of the Github repo, feedbacks are welcome => https://github.com/GregoryOtt/UpdateWiBuildNum/blob/master/Update-GlobalList.ps1
[disclaimer - I work on the new build system]
That global list on the workitem is a mechanism that dated back to the original release of TFS. It's one that sort of worked in that day and age (days of nightly builds, pre-CI and CD agility). It's starts to fall apart and doesn't show as proper relationships in TFS. I worked on WIT at that time and we needed a queryable mechanism and that's what we had (blame me :)
So, when we started a new build system, we didn't want to rebuild things and repeat the same mistakes. We're trying to take an agile, incremental approach to a better build system.
In the next sprint (88), we are starting work on proper links between builds and workitems and the WIT team is also doing work to make them more first class. The first thing you'll see is a link on the WIT form and that should hopefully make QU1 as well (at least parts of it).
We realize this does leave a few gaps but we are working to close them (gated and label sources being two others) and hopefully in a better way for a better long term product.
As far as a workaround goes, it should be possible to automate via powershell and our clients but we don't have anything canned for others to use.

Effective permissions on remote share for domain users in Powershell

I searched and read some topics here but I didn't found what I am looking for.
Basically, I want to check the effective permissions for a specific user for several shares, I want a script such as :
$user = Read-Host "Enter username"
$shares = "\\serverABC\share2","\\serverABC\share1"
foreach ($share in $shares)
{
Cmdlet-EffectivePermissions $share
}
Output expected :
\\serverABC\share1
Full Control : No
Traverse folder / execute / file : YEs
List folder / read data : No
...
\\serverABC\share2"
Full Control : No
Traverse folder / execute / file : YEs
List folder / read data : No
...
In fact, I want to do in Powershell exactly the same way that effective permissions Tab.
Does it exist a built-in solution (without importing any modules, add-ins, ...) with .NET Method (GetUserEffectivePermissions) or with Get-ACL?
I'm not aware of a .NET/PowerShell way to do this natively. There is a PowerShell module here that should be able to do what you're looking for, though. After importing that, you should be able to modify your pseudo code to the following:
$user = Read-Host "Enter username"
$shares = "\\serverABC\share2","\\serverABC\share1"
foreach ($share in $shares) {
Get-EffectiveAccess -Path $share -Principal $user -ListAllRights
}
That returns PS objects instead of simple text. If the format isn't to your liking, you can use some of the utility commands to shape it however you like. Here are two examples of doing that:
First, a simple change to original that doesn't return the exact format you mentioned, but it's pretty close:
foreach ($share in $shares) {
$share
Get-EffectiveAccess -Path $share -Principal $user -ListAllRights | ForEach-Object {
"{0}: {1}" -f $_.Permission, $_.Allowed
}
""
}
Next, a more complicated change that formats the output exactly how you were asking (at least I think):
# Go through each FileSystemRights enum name and add them to a hash table if their value is
# a power of 2. This will also keep track of names that share a value, and later those can
# be combined to provide a friendly permission name
$Ht = #{}
foreach ($Name in [System.Enum]::GetNames([System.Security.AccessControl.FileSystemRights])) {
$Value = [System.Security.AccessControl.FileSystemRights]::$Name
if ($Value.value__ -band ($Value.value__ - 1)) {
# Not a power of 2, so ignore this
continue
}
if (-not $Ht.$Value) {
$Ht.$Value = #()
}
$Ht.$Value += $Name
}
# FullControl isn't a power of 2, but it's useful to test for access, so add it manually
$Ht.([System.Security.AccessControl.FileSystemRights]::FullControl) = "FullControl"
function YesNoTest {
param(
[System.Security.AccessControl.FileSystemRights] $EffectiveAccess,
[System.Security.AccessControl.FileSystemRights] $AccessToTest
)
if (($EffectiveAccess -band $AccessToTest) -eq $AccessToTest) {
"Yes"
}
else {
"No"
}
}
$shares | Get-EffectiveAccess -Principal $user | ForEach-Object {
$_.DisplayName
$EffectiveAccess = $_.EffectiveAccess
$Ht.GetEnumerator() | sort { $_.Key.value__ } -Descending | ForEach-Object {
"{0}: {1}" -f ($_.Value -join " / "), (YesNoTest $EffectiveAccess $_.Key)
}
""
}
Note that this won't be completely accurate if you run this against a remote system and the following conditions are met:
The security descriptor contains groups that are local to the remote system, i.e., non domain groups
The user(s) you're checking is a member of one of the local groups