GET-ACL and “ReadandExecute” versus List

I find it a lot easier to do virtually all of my work via the keyboard.  Using PS ISE I can essentially make a log of everything I work on during the day. There are a few things where I have to resort to using a GUI but I’m learning how to get around more and more of those.

One of the areas I learned a while back was using GET-ACL in order to find the NTFS security on a shared folder in order to be able to see what AD group a person would need to be in for access. In case you haven’t used that it’s essentially something like this

get-acl $fldrpath | fl AccessToString

It works great – until you hit a situation where the real permission is List. Then it’s confusing:

Everyone Allow ReadAndExecute, Synchronize

Looks just like Read-only access.

After a little searching around I was able to find that there is a way with PowerShell to get the correct List entry – the inheritanceflags on List and Read-Only differ. List has only the inheritance flag “ContainerInherit” while Read has “ContainerInherit,ObjectInherit”. Once I updated my quicky script to include some extra logic to check for that and presto

Everyone ----------------------------------------> Allow -----> ListDirectory

Much better 🙂

Using PowerShell to get a list of shares from a server

This one is relatively easy on first glance.

$shares = Get-WmiObject -ComputerName SERVERNAME -class win32_Share

The win32_Share class gets the shares as listed by WMI. From here normally you can get the permissions fairly easily. Except in a case like this:

Name Path Description
---- ---- -----------
ADMIN$ C:\Windows Remote Admin
C$ C:\ Default share
E$ E:\ Default share
F$ F:\ Default share
G$ G:\ Default share
\\SERVER-MSDTC\M$ M:\ Cluster Default Share
H$ H:\ Default share
IPC$ Remote IPC
\\SERVERSHR-CLS\ClusterStorage$ C:\ClusterStorage Cluster Shared Volumes Default Share
M$ M:\ Default share
\\SERVERSHR-CLS\Q$ Q:\ Cluster Default Share
O$ O:\ Default share
\\SERVERSHR-SQL\F$ F:\ Cluster Default Share
\\SERVERSHR-SQL\FILES F:\files
Q$ Q:\ Default share
\\SERVERSHR-SQL\G$ G:\ Cluster Default Share
\\SERVERSHR-SQL\H$ H:\ Cluster Default Share
LogFiles C:\Program Files\Microsoft SQL Server\MSRS11.RPT\Reporting Services\LogFiles
\\SERVERSHR-SQL\App F:\app
\\SERVERSHR-SQL\bin F:\\bin
\\SERVERSHR-SQL\O$ O:\ Cluster Default Share
\\SERVERSHR-SQL\Files2 F:\Files2
\\SERVERSHR-SQL\Files3 H:\Files3

If we were looping through trying to do something like this on each share

$ShareSec = Get-WmiObject -Class Win32_LogicalShareSecuritySetting -ComputerName $($ServerReportingOn.DNSHostname) -Filter "Name='$sharetocheck'"

We’d get errors that would look like this.

+ ... $ShareSec = Get-WmiObject -Class Win32_LogicalShareSecuritySetting -C ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Get-WmiObject], ManagementException
+ FullyQualifiedErrorId : GetWMIManagementException,Microsoft.PowerShell.Commands.GetWmiObjectCommand

The solution is simple. Notice how all the cluster shares start “\\”? In your loop to get the permissions you would check the share name for “\\” and skip that “Get-WmiObject -Class Win32_LogicalShareSecuritySetting” line for any share name starting with it.

Office365 and dirsync – the multiple accounts with the same UPN/mail/proxyaddresses value

For everyone who is working on an Office365 email deployment and using dirsync you are probably familiar with dirsync errors and trying to find the duplicated proxy addresses within AD. For everyone who hasn’t started one of these there are several things which must be unique within the FOREST. Proxy addresses is one of those things that must be unique within the directory. Dir sync will find instances of duplicated proxy addresses and will error on those objects.

Running into this at work I decided to see if someone had done the heavy-lifting of writing a script before me to find duplicated proxy address, mail, and UPN values. I didn’t find anything doing a quick Google that suited my needs and wants.

What this does is it goes out and pulls all of the user, contact, and group objects within the forest, selecting the canonical name, mail, UPN, and proxy addresses values for each. It then puts all of that into a single array. A prompt is put in for what string to look for. Once that is entered a straight search through all of the collected objects is performed with the results of any matches displayed. The input loop is then repeated so I don’t have to recollect all the data each time.

# This code is deliberately inefficient on the Get-ADObject command. The purpose being so the script can be adapted for other
# duplicate searches, such as for the mail user object property being non-unique. One advantage to this code is that it looks for
# all user and contact objects and gets their UPNS, mail, and proxyaddresses values, rather than just those with homeMDB populated. I have seen some
# accounts that have had the mailboxes rudely disassociated leaving proxyaddresses values that are not searchable via EMC/EMS.
#
$domainlist= (get-adforest).domains
foreach ($d in $domainlist)
    {
        Write-host "Processing domain " $d ". Please be patient. This may take some time depending on the number of user objects."
        $userlist = get-adobject -LDAPFilter "(&(objectClass=User)(objectCategory=person))" -Server $d -properties canonicalname,proxyaddresses,mail,userprincipalname | select canonicalname,proxyaddresses,mail,userprincipalname
        $contactlist = get-adobject -LDAPFilter "objectClass=Contact" -Server $d -properties canonicalname,proxyaddresses,mail,userprincipalname | select canonicalname,proxyaddresses,mail,userprincipalname
        $grouplist = get-adobject -LDAPFilter "objectClass=group" -Server $d -properties canonicalname,proxyaddresses,mail,userprincipalname | select canonicalname,proxyaddresses,mail,userprincipalname
	    foreach ($ul in $userlist)
	        {
		        [array]$allobjs += $ul
	        }
	    foreach ($cl in $contactlist)
	        {
		        [array]$allobjs += $cl
	        }
	    foreach ($gl in $grouplist)
	        {
		    [array]$allobjs += $gl
	        }
    }
$total = $allobjs.count
write-host " "
write-host "-------------------"
write-host "Total user and contact objects collected : " $total
$count = 1
$MatchingObjs = $null
write-host " "
write-host "-------------------"
$address= read-Host "Enter search address. Hit ENTER or type exit to exit. : "
If (($address.Length -gt 0) -and ($address -ne "exit"))
    {
        Do
            {
                foreach ($ao in $allobjs)
                    {
                        Write-Progress -Activity "Scanning for" $address -PercentComplete ($count/$total * 100)
                        $MatchFound = $False
                        ForEach ($pa in $ao.ProxyAddresses) 
                            {
                                If ($pa –Match $address)
                                    {
                                        $MatchFound = $True
                                    }
                            }
                        #add matches to array 
                        If ($ao.mail –Match $address)
                            {
                                $MatchFound = $True
                            }
                        If ($ao.userprincipalname –Match $address)
                            {
                                $MatchFound = $True
                            }
                        If ($MatchFound) 
                            {
                                [array]$MatchingObjs += $ao
                            }
                        $count++
                    }
                write-host " "
                write-host "-------------------"
                Write-host "Matching objects:"
                write-host "-------------------"
                foreach ($mo in $MatchingObjs)
                    {
                        write-host $mo.canonicalname
                    }
                write-host "-------------------"
                write-host " "
                $count = 1
                $MatchingObjs = $null
                $address = read-Host "Enter search address. Hit ENTER or type exit to exit. : "
            }
        Until (($address.Length -eq 0) -or ($address -eq "exit"))
    }

Sending multiple command outputs to a single Out-GridView

I’ll admit – I’ve been very remiss in keeping this up of late. To make up for that I’m going to try to make sure to do three posts a week.

Since it’s now right at 2155 I won’t be able to do anything continuing my previous posts. However, I will provide a handy tip.

Frequently I have come across needing to get information from several accounts and put them into a spreadsheet. Often there isn’t sufficient need to build a huge script to set things up, so what I do is this.

I start with and array type variable and add my first set of values like this:

[array]$a = get-qaduser Joe | select-object logonname,lastname,firstname,accountexpires

From here I add each successive value:

$a += get-qaduser Bob | select-object logonname,lastname,firstname,accountexpires
 $a += get-qaduser Fred | select-object logonname,lastname,firstname,accountexpires

Now I can send it to Out-GridView:

$a | Out-GridView -OutputMode multiple

I now have a nice output window where I can copy the values straight to an Excel workbook!

 

Enjoy!

Interlude II : Excel with PowerShell

In my last post I finished with this code:

$xl = New-Object -ComObject "Excel.Application"
$xl.visible = $true
$xlbook = $xl.workbooks.Add()
$xlbook.worksheets.Add() | out-null
$xlsheets = $xlbook.worksheets
$xlsheet1 = $xlsheets.item(1)
$xlsheet1.name = "Report"
$xlsheet1.Cells.Item(1,1) = "Header1"
$xlsheet1.Cells.Item(1,2) = "Header2"
$xlsheet1.Cells.Item(1,3) = "Header3"
$xlbook.SaveAs("REPORT.XLSX")
$xl.quit() | Out-Null

As I said, this can be made much better looking for a report. In addition, we can do things a bit smarter to make our job easier down the road.

First off, let’s break our code up into functions.

Function NewExcelObj
     {
          Set-Variable -Name xl -Value (New-Object -ComObject "Excel.Application") -Scope 1
          $xl.visible = $true
          Set-Variable -Name xlbook -Value ($xl.workbooks.Add()) -Scope 1
         $xlbook.worksheets.Add() | out-null
     }
Function AddSheets
     {
          Set-Variable -Name xlsheet -Value ($xlbook.worksheets) -Scope 1
          Set-Variable -Name xlsheet1 -Value ($xlsheet.item(1)) -Scope 1
          $xlsheet1.name = "Report"
     }
Function AddHeaders
     {
          $xlsheet1.Cells.Item(1,1) = "Header1"
          $xlsheet1.Cells.Item(1,2) = "Header2"
          $xlsheet1.Cells.Item(1,3) = "Header3"
     }
Function CloseExcel
     {
          $xlbook.SaveAs("REPORT.XLSX")
          $xl.quit() | Out-Null
     }
# Main code
NewExcelObj
AddSheets
AddHeaders
CloseExcel

What we’ve done is moved all of our code into functions. Note we had to change how certain variables are defined.

In PowerShell there are different scopes. Variables, objects, and constants reside within the scopes. Variables (note: I’m using this as shorthand to include objects and constants as well) defined in the main script are available to all levels of the script. A variable defined within a function is normally available to just that function’s scope. When the code returns from the function the variable defined within the function is lost.

Because of that you’ll see I have redone the definitions of some of the items used previously . Instead of the normal “$Variable = value” methodology I have swapped in “Set-Variable –Name Variable –Value (value) –Scope 1”. Using this format allows me to make use of the Scope option which allows me to say that the variable is defined at the scope 1 level above the function scope, in this case the base script scope level. With the variables created that way I can proceed to make use of the variables in other functions.

This cleans up the code some, but what about the spreadsheet. It’s still as ugly as before.

Let’s do something about that:

# Main code
NewExcelObj
AddSheets
[array]$headerlist = ”Header1”,”Header2”,”Header3”
AddHeaders $headerlist
CloseExcel
Function AddHeaders ($AHhl)
     {
          $col = 1
          Foreach ($t in $AHhl)
               {
                    $xlsheet1.Cells.Item(1,$col) = $t
                    $xlsheet1.Cells.Item(1,$col).Font.Bold = $True
                    $xlsheet1.Cells.Item(1,$col).Interior.ColorIndex = 15
                    $col++
               }
     }

post4

We are doing A lot of things here at once. First, just before the call of the function AddHeaders we are defining an array that contains our header values. We then pass that variable to AddHeaders – notice the line for the call of AddHeaders now is “AddHeaders $headerlist” and our Function header is “Function AddHeaders ($AHhl)” where $AHhl is contains the passed array values of $headerlist. We now have a function-local variable called $col to keep track of what column we are in. We go into a standard Foreach loop where we step through each member of $AHhl, populate the header column, and move on with incrementing $col ($col++). Between the two lines populating and incrementing are two lines that contain formatting instructions for the cells we just touched. The first sets the font in the cell to bold, the second sets a fill colour in the cell of a light grey.

That makes our headers stand out:

There’s still one more thing we should do to our sheet – autofit the columns. There is nothing more annoying to people looking at a spreadsheet than having to change the column width. We can accomplish this with just two lines of code added to our CloseExcel function.

Function CloseExcel
     {
         $UsedRange = $xlsheet1.UsedRange
         [void] $UsedRange.EntireColumn.Autofit()
         $xlbook.SaveAs("REPORT.XLSX")
         $xl.quit() | Out-Null
     }

We define a variable in the scope of the function called $UsedRange and assign the used range property of the sheet to it. After that we use the Autofit() method on the property EntireColumn forcing all of the used columns to be autofit to the data within the column. Finally we perform our previously defined quit statements, saving and closing the workbook.

Interlude I : Excel with PowerShell

One of the things that comes up frequently is being able to provide auditors with documentation. Often the result is something that can be done in Excel. With PowerShell this can be done as part of a script.

First we need to recognize that the machine generating the Excel document will need Excel installed. No Excel, no document using this method.

First we need to start by creating a new COM object instance of Excel:

$xl = New-Object - ComObject "Excel.Application"

So we can watch as the document in populated by the script we add in the following:

$xl.visible = $true

post1

With this we just get a nice empty Excel window. Nothing at all going on. First we need to add a workbook and then sheets to the workbook.

$xlbook = $xl.workbooks.Add()
$xlbook.worksheets.Add() | out-null

post2

That’s looking better. Now we create an object to reference the sheets within the workbook more easily:

$xlsheets = $xlbook.worksheets
$xlsheet1 = $xlsheets.item(1)
$xlsheet1.name = "Report"

What we’ve done is reduce our typing so that we don’t have to type $xlbook.Worksheets.item(1).name. It also makes more sense to us slow-brained humans than a long string of properties run together – translation : lower risk of error.

Now we can add our column headers.

$xlsheet1.Cells.Item(1,1) = "Header1"
$xlsheet1.Cells.Item(1,2) = "Header2"
$xlsheet1.Cells.Item(1,3) = "Header3"

post3

It should be obvious in the above that the value “1,1” refers to row 1 cell 1, “1,2” to row 1 cell 2, etc. Knowing this it should be easy to figure out how to place data into cells within the sheet.

All of this is kind of useful but what would be the point of scripting all up to this point if you don’t go ahead and save the sheet. That takes just two lines of code:

$xlbook.SaveAs("REPORT.XLSX")
$xl.quit() | Out-Null

The code talked about in this post:

$xl = New-Object -ComObject "Excel.Application"
$xl.visible = $true
$xlbook = $xl.workbooks.Add()
$xlbook.worksheets.Add() | out-null
$xlsheets = $xlbook.worksheets
$xlsheet1 = $xlsheets.item(1)
$xlsheet1.name = "Report"
$xlsheet1.Cells.Item(1,1) = "Header1"
$xlsheet1.Cells.Item(1,2) = "Header2"
$xlsheet1.Cells.Item(1,3) = "Header3"
$xlbook.SaveAs("REPORT.XLSX")
$xl.quit() | Out-Null

Next post I’ll talk about how to make this better looking.

Getting the members of a local group via PowerShell, Part II

In my last post I showed how after several steps we ran

(Get-WmiObject win32_groupuser -Filter $query).PartComponent

and got

\\PC\root\cimv2:Win32_UserAccount.Domain="PC",Name="Administrator"
\\PC\root\cimv2:Win32_Group.Domain="Domain",Name="Desktop Admins"
\\PC\root\cimv2:Win32_Group.Domain="Domain",Name="Domain Admins"
\\PC\root\cimv2:Win32_UserAccount.Domain="Domain",Name="ME"

Now we’re going to clean this up into a format that is more readable. First let’s take that last command and assign it to a variable:

$list = (Get-WmiObject win32_groupuser -Filter $query).PartComponent

This variable, $list, is actually an array. As such we can now create a foreach loop and process each line. The first thing we’re going to do is isolate the domain portion out.

foreach ($l in $list)
   {
       $domain = $l.Substring($l.IndexOf("`"")+1)
       $domain = $domain.Substring(0,$domain.IndexOf("`""))
       $domain | Out-Default
   }

So what we have done is get the domain out. Looking at the first line where the variable $domain is set we found the first case of the double quote character and removed everything to the left of it by first using the IndexOf method to find the position of the ” and then the Substring method to crop off the data to the left of it. By adding 1 to the IndexOf value we made sure to include the ” in the removed text. Note I had to use the sequence `” (NOT ‘ – ` which is the key to the left of the 1 key on a US keyboard) to be able to include the ” character in the search string. The next line involves finding the next ” and removing everything to the right of it. So our output from the loop is:

PC
Domain
Domain
Domain

Now let’s add some code to get the user/group name.

foreach ($l in $list)
   {
      $domain = $l.Substring($l.IndexOf("`"")+1)
      $UG = $domain
      $domain = $domain.Substring(0,$domain.IndexOf("`""))
      $UG = $UG.Substring($UG.IndexOf("`"")+1)
      $UG = $UG.Substring($UG.IndexOf("`"")+1)
      $UG = $UG.Substring(0,($UG.Length-1))
      $domain + "\" + $UG | Out-Default
   }

We’ve added a new variable $UG. Our first step is to capture the portion of the string after the section to the first ” is stripped  off – we need the value at the end. The next time we set $UG we are stripping everything up to and including the second ” in the original string. Then we repeat to the third ” in the original. Finally we strip off the the last “. Adding a “\” and the $UG value to our output gives us this:

PC\Administrator
Domain\Desktop Admins
Domain\Domain Admins
Domain\ME

Next time I’ll go over building a report for this information so it can be handed off to auditors.