2013 Scripting Games Event 4

Instructions:

  • Step 1: Scoping the event
  • The requirements don’t tell:

      • what operating system will be used to run the code
      • what version of powershell will be used to execute the code
      • whether the Active Directory module is present and loaded
      • what version of Windows Domain Controllers are running
      • how the Active Directory is configured: replication,…

    I made 3 assumptions for this event:

      • We won’t use PowerShell V1 because Get-Random cmdlet isn’t available. Writing a piece of code to simulate the behavior of Get-Random would have been interesting but was totally out-of-scope for this event. Anyway this doesn’t mean that we cannot come up with clever ways of selecting 20 pseudo randomly users.
      • If the domain admin that will run the code has installed Active Directory modules for Powershell (using RSAT) and still has only Active Directory running on Windows 2003, he has also taken care of installing Active Directory Management Gateway Service. The domain admin will not run the code on a broken plateform.
      • The instructions say “A Domain Admin will always run the command”. It doesn’t tell if he will run the code from a computer that doesn’t belong to Active Directory. I assumed he will select a machine (client or server) that belongs to an Active Directory because that’s just common sense. Checking that the computer is a member of a domain and not a workgroup isn’t a requirement.
  • Step 2 Gathering information on Active Directory properties
  • One of the most difficult tasks in this event is that there are many ways to query Active Directory, some techniques return the raw value and some display a nicely formatted value. They don’t have the same properties name and count returned by default.

    The Get-ADUser cmdlet returns by default 10 properties. Using core cmdlets like Get-Item on a user object while navigating the AD drive will return only 4 properties by default.
    Get-ADUser will display a nicely formatted property PasswordLastSet that is derived from the 64bit integer value named pwdLastSet…

    To make it even more difficult, you should find out how accurrate these properties are, whether they are replicated across all domain controllers, how active directory stores them and how some of these properties are calculated. One of the most common mistake is to use the LastLogon property. This property isn’t replicated across DCs, it’s thus unreliable. The lastlogontimestamp is replicated but it’s not 100% accurate.

    To illustrate that and to get more information about Active Directory, I’ve got some really nice links to share from the following sources:

  • Step 3: Organize scripting tasks
    • Testing whether the Active Directory module is present or not
    • To be absolutely sure that the orginal module from Microsoft is present and being used, I first remove any module named “ActiveDirectory”.

      Remove-Module -Name "ActiveDirectory" -Force -ErrorAction Stop

      Then I try to load the original module from Microsoft. As I don’t know if the $env:PSModulePath has been modified, I try to load the module from its original path. Yes, the Import-Module cmdlet allows filepath as input.

      Get-Item "$env:systemroot\system32\WindowsPowerShell\v1.0\Modules\ActiveDirectory" -EA Stop |            
      Import-Module -ErrorAction Stop            
      
    • Testing whether if the Active Directory drive is mounted or not
    • For fun, I decided to use the core cmdlets (Get-ChildItem, Get-Item,…) to query the AD: drive. I needed to know whether the drive was mounted along with the module import or disabled on purpose using the $Env:ADPS_LoadDefaultDrive variable:

      if (Test-Path Env:ADPS_LoadDefaultDrive) {             
          if ([bool]($Env:ADPS_LoadDefaultDrive)) {            
              # The drive is loaded because the value is not 0            
              $ADDriveLoaded = $true            
              Write-Verbose "AD drive mounted by default using environment variable"            
          } else {            
              # $Env:ADPS_LoadDefaultDrive = 0 -> don't load            
              Write-Verbose "AD drive mounting was disabled using environment variable"            
              if ($Fun) {            
                  try {            
                      # Mount the drive using a Global Catalog            
                      New-PSDrive -PSProvider ActiveDirectory -Name AD -Root "" -server (            
                          Get-ADDomainController -Discover -Service 2 -ErrorAction Stop            
                      ).Name -ErrorAction Stop            
                      $ADDriveLoaded = $true            
                      Write-Verbose "AD drive mounted in current scope"            
                  } catch {            
                      $ADDriveLoaded = $false            
                      Write-Warning -Message "Failed to mount AD drive in current scope because  $($_.Exception.Message)"            
                  }            
              } else {            
                  $ADDriveLoaded = $false            
              }            
          }            
      } else {            
          # The AD drive is loaded by default            
          $ADDriveLoaded = $true            
          Write-Verbose "AD drive mounted by default"            
      }
    • Avoid the recursive search gotcha
    • I have noticed during the testing phase, that the Active Directory provider no longer allow a Recursive search in Powershell 3.0 as it did in Powershell 2.0
      I had to add a mandatory path to an OU to workaround that issue.

      if ($Fun -and $ADDriveLoaded) {            
          Write-Verbose "Using the Active Directory module in fun mode"            
          Switch ($PSVersionTable.PSVersion.Major) {            
              2 {            
                  $GCIHT = @{            
                      Path = "AD:\$Path"            
                      Recurse = $true            
                  }            
                  break            
              }            
              default {            
                  $GCIHT = @{Path = "AD:\$Path"}            
              }            
          }            
      $allusers = @(Get-ChildItem -Filter "(&(objectClass=user)(objectCategory=person))" @GCIHT)            
      
    • Validating an OU path
    • I could not validate the path of the OU in the parameter block as I needed to know whether the AD drive was mounted or not. I added the following in the Begin block:

      # Validate the OU            
      if ($Fun -and $ADDriveLoaded) {            
          if (-not(Test-Path -Path "AD:\$($Path)" -PathType Container)) {            
              Write-Warning "Invalid OU path provided"            
              break            
          }            
      }
    • Solving the case of the User-Account-Control attribute and other Active Directory attibutes
    • Can the userAccountControl attribute be used to determine if the user account is locked or if his password is expired?
      The KB article kb305144 says that:

      Here’s also a comment on the MSDN page about the userAccountControl

      Thanks to the Powershell magazine PSTip I have seen that the msDS-User-Account-Control-Computed AD attribute is a constructed attribute

      Thanks to Richard Mueller and his wiki page we also know how these attributes are calculated:








      As you can see, this wiki page is just awsome, it actually answers many questions 😎

      When using ADSI or the AD: drive, I’ve used this kind of code to display nicely formatted properties:

      1..$MaxUsers | ForEach-Object {            
          $i = (Get-Random -Maximum $MaxRandom  -Minimum 0)            
          $propsar= 'sAMAccountName','department','title','userAccountControl','lastLogonTimestamp',            
                      'pwdLastSet','msDS-User-Account-Control-Computed'            
          $user = Get-Item -Path "AD:\$($allusers[$i].distinguishedName)" -Properties $propsar            
          New-Object -TypeName PSObject -Property @{            
              UserName = $user.sAMAccountName            
              Enabled = -not($user.userAccountControl -band 2)            
              Department = $user.department            
              Title = $user.title            
              LastLogonDate = switch ($user.lastLogonTimestamp) {            
                  $null {            
                      'Never' ; break            
                  }            
                  default {            
                      [datetime]::FromFileTime([int64]::Parse($_))            
                  }            
              }            
              LockedOut = [bool]($user.'msDS-User-Account-Control-Computed' -band 16)            
              PasswordExpired = [bool]($user.'msDS-User-Account-Control-Computed'-band 8388608)            
              PasswordLastSet = switch ($user.pwdLastSet) {            
                  0 {            
                          if ($user.userAccountControl -band 65536) {            
                              0 ; break            
                          } else {            
                              'Must Change password at next logon' ; break            
                          }            
                  }            
                  default {            
                      [datetime]::FromFileTime([int64]::Parse($_))            
                  }            
              }            
          }            
      } |             
      Select -Property UserName,Enabled,Department,Title,LastLogonDate,LockedOut,PasswordExpired,PasswordLastSet |            
      Sort -Descending -Property PasswordLastSet

      Having the PwdLastSet attribute set to 0 doesn’t necessarily means that the user must change his password at next logon
      (source)

    • Making sure to get results quickly
    • Performance wasn’t a requirement. But I didn’t know how many user objects are present in Active Directory. We could have 1000, 10 000 or even more. To get results more quickly, I added a range to the MaxUsers parameter and used its upper limit as a value for the ResultSetSize parameter of the Get-ADUser cmdlet.

      [parameter()]            
      [ValidateRange(0,1000)]            
      [int]$MaxUsers=20
      $allusers = ActiveDirectory\Get-ADUser -Filter * -ResultSetSize 1000

      ADSI has also a way to set the maximum number of objects returned.

      $Searcher = [System.DirectoryServices.DirectorySearcher]$Root            
      $Searcher.SizeLimit = 1000

      Unfortunately, we cannot instruct core cmdlets to get a certain number of items. As I discovered the issue with the recursive search and made the OU path a mandatory parameter, I could get 0 or more AD users objects. Yes, I could get 0 object if you type a valid OU path where there isn’t any user object. Worse, I noticed another problem when there’s only 1 user object. My Get-Random cmdlet was throwing an error because you can’t do:

      Get-Random -Maximum 1 -Minimum 1

    • (Re)using code to produce the HTML page
    • In the previous event, I used the awsome work of Don Jones who wrote a free ebook named “Creating HTML Reports in PowerShell” that you can download on this page: http://powershell.org/wp/newsletter/

    • Working on parameter validation
    • The instruction said that:

      Get PowerShell to do as much of that validation as possible – Dr.Scripto has to review your code, and doesn’t want to read a lot of manual validation
      script.

      I spent some time testing and reading the help about ValidateScript.
      I think I found a way to put as much as validation as possible in the ValidateScript attribute, keep it easy to read, grasp the logic and of course maintain.
      The only thing I wanted was to make the error behave as if it was thrown by a cmdlet.
      I wanted to avoid the full ValidateScript content to be displayed in the console when an error occurs because having too many lines could be somehow “counter-productive”. I’m also not a big fan of using the throw keyword… but sometimes you just need it πŸ˜‰

      [parameter()]            
      [ValidateScript({            
          # Test that the parent path exists            
          if (-not(Test-Path -Path (Split-Path -Path $_ -Parent) -PathType Container)) {            
              throw New-Object System.ArgumentException -ArgumentList "Filepath doesn't exist"            
          }            
          # Validate the filename extension            
          if ( (Split-Path -Path $_ -Leaf) -notmatch ".*\.html?$") {            
              throw New-Object System.ArgumentException -ArgumentList "File extension must end by .html or .htm"            
          }            
          return $true            
      })]            
      [string]$FilePath = "$Env:TEMP\audit.report.html"

      And here’s the result

      Not bad? What do you think?

    Final word
    This event was really hard but also fun. It was a good opportunity to learn many many things about Active Directory, like ADSI,…
    I’m not super proud of how my entry looks like but I know it does the job.
    You can have a look at it: http://scriptinggames.org/entrylist_.php?entryid=914
    If you see anything that’s wrong or that I can improve, I welcome your feedback. If you find something you don’t understand, leave a comment on this blog post, I’ll try to explain it.

    Advertisements

    Leave a Reply

    Fill in your details below or click an icon to log in:

    WordPress.com Logo

    You are commenting using your WordPress.com account. Log Out / Change )

    Twitter picture

    You are commenting using your Twitter account. Log Out / Change )

    Facebook photo

    You are commenting using your Facebook account. Log Out / Change )

    Google+ photo

    You are commenting using your Google+ account. Log Out / Change )

    Connecting to %s