class Inspec::Resources::WindowsUser

This optimization was inspired by @see mcpmag.com/articles/2015/04/15/reporting-on-local-accounts.aspx Alternative solutions are WMI Win32_UserAccount @see msdn.microsoft.com/en-us/library/aa394507(v=vs.85).aspx @see msdn.microsoft.com/en-us/library/aa394153(v=vs.85).aspx

Public Instance Methods

collect_user_details() click to toggle source

msdn.microsoft.com/en-us/library/aa746340(v=vs.85).aspx

# File lib/inspec/resources/users.rb, line 664
    def collect_user_details # rubocop:disable Metrics/MethodLength
      return @users_cache if defined?(@users_cache)

      script = <<~EOH
        Function ConvertTo-SID { Param([byte[]]$BinarySID)
          (New-Object System.Security.Principal.SecurityIdentifier($BinarySID,0)).Value
        }

        Function Convert-UserFlag { Param  ($UserFlag)
          $List = @()
          Switch ($UserFlag) {
            ($UserFlag -BOR 0x0001) { $List += 'SCRIPT' }
            ($UserFlag -BOR 0x0002) { $List += 'ACCOUNTDISABLE' }
            ($UserFlag -BOR 0x0008) { $List += 'HOMEDIR_REQUIRED' }
            ($UserFlag -BOR 0x0010) { $List += 'LOCKOUT' }
            ($UserFlag -BOR 0x0020) { $List += 'PASSWD_NOTREQD' }
            ($UserFlag -BOR 0x0040) { $List += 'PASSWD_CANT_CHANGE' }
            ($UserFlag -BOR 0x0080) { $List += 'ENCRYPTED_TEXT_PWD_ALLOWED' }
            ($UserFlag -BOR 0x0100) { $List += 'TEMP_DUPLICATE_ACCOUNT' }
            ($UserFlag -BOR 0x0200) { $List += 'NORMAL_ACCOUNT' }
            ($UserFlag -BOR 0x0800) { $List += 'INTERDOMAIN_TRUST_ACCOUNT' }
            ($UserFlag -BOR 0x1000) { $List += 'WORKSTATION_TRUST_ACCOUNT' }
            ($UserFlag -BOR 0x2000) { $List += 'SERVER_TRUST_ACCOUNT' }
            ($UserFlag -BOR 0x10000) { $List += 'DONT_EXPIRE_PASSWORD' }
            ($UserFlag -BOR 0x20000) { $List += 'MNS_LOGON_ACCOUNT' }
            ($UserFlag -BOR 0x40000) { $List += 'SMARTCARD_REQUIRED' }
            ($UserFlag -BOR 0x80000) { $List += 'TRUSTED_FOR_DELEGATION' }
            ($UserFlag -BOR 0x100000) { $List += 'NOT_DELEGATED' }
            ($UserFlag -BOR 0x200000) { $List += 'USE_DES_KEY_ONLY' }
            ($UserFlag -BOR 0x400000) { $List += 'DONT_REQ_PREAUTH' }
            ($UserFlag -BOR 0x800000) { $List += 'PASSWORD_EXPIRED' }
            ($UserFlag -BOR 0x1000000) { $List += 'TRUSTED_TO_AUTH_FOR_DELEGATION' }
            ($UserFlag -BOR 0x04000000) { $List += 'PARTIAL_SECRETS_ACCOUNT' }
          }
          $List
        }

        $Computername = $Env:Computername
        $adsi = [ADSI]"WinNT://$Computername"
        $adsi.Children | where {$_.SchemaClassName -eq 'user'} | ForEach {
          New-Object PSObject -property @{
            uid = ConvertTo-SID -BinarySID $_.ObjectSID[0]
            username = $_.Name[0]
            description = $_.Description[0]
            disabled = $_.AccountDisabled[0]
            userflags = Convert-UserFlag  -UserFlag $_.UserFlags[0]
            passwordage = [math]::Round($_.PasswordAge[0]/86400)
            minpasswordlength = $_.MinPasswordLength[0]
            mindays = [math]::Round($_.MinPasswordAge[0]/86400)
            maxdays = [math]::Round($_.MaxPasswordAge[0]/86400)
            warndays = $null
            badpasswordattempts = $_.BadPasswordAttempts[0]
            maxbadpasswords = $_.MaxBadPasswordsAllowed[0]
            gid = $null
            group = $null
            groups = @($_.Groups() | Foreach-Object { $_.GetType().InvokeMember('Name', 'GetProperty', $null, $_, $null) })
            home = $_.HomeDirectory[0]
            shell = $null
            domain = $Computername
            lastlogin = if($_.lastlogin.getType().Tostring() -eq "System.Management.Automation.PSMethod" ){ $null }else{[String]$_.lastlogin}
          }
        } | ConvertTo-Json
      EOH
      cmd = inspec.powershell(script)
      # cannot rely on exit code for now, successful command returns exit code 1
      # return nil if cmd.exit_status != 0, try to parse json
      begin
        users = JSON.parse(cmd.stdout)
      rescue JSON::ParserError => _e
        return nil
      end

      # ensure we have an array of groups
      users = [users] unless users.is_a?(Array)
      # convert keys to symbols
      @users_cache = users.map { |user| user.each_with_object({}) { |(k, v), h| h[k.to_sym] = v } }
    end
credentials(username) click to toggle source
# File lib/inspec/resources/users.rb, line 643
def credentials(username)
  res = identity(username)

  return if res.nil?

  {
    mindays: res[:mindays],
    maxdays: res[:maxdays],
    warndays: res[:warndays],
    badpasswordattempts: res[:badpasswordattempts],
    maxbadpasswords: res[:maxbadpasswords],
    minpasswordlength: res[:minpasswordlength],
    passwordage: res[:passwordage],
  }
end
identity(username) click to toggle source
# File lib/inspec/resources/users.rb, line 620
def identity(username)
  # TODO: we look for local users only at this point
  name, _domain = parse_windows_account(username)
  return if collect_user_details.nil?

  res = collect_user_details.select { |user| user[:username] == name }
  res[0] unless res.empty?
end
list_users() click to toggle source
# File lib/inspec/resources/users.rb, line 659
def list_users
  collect_user_details.map { |user| user[:username] }
end
meta_info(username) click to toggle source
# File lib/inspec/resources/users.rb, line 629
def meta_info(username)
  res = identity(username)

  return if res.nil?

  {
    home: res[:home],
    shell: res[:shell],
    domain: res[:domain],
    userflags: res[:userflags],
    lastlogin: res[:lastlogin],
  }
end
parse_windows_account(username) click to toggle source
# File lib/inspec/resources/users.rb, line 613
def parse_windows_account(username)
  account = username.split("\\")
  name = account.pop
  domain = account.pop unless account.empty?
  [name, domain]
end