All expired user accounts and the accounts that are about to expire

A colleague of mine asked me to write a script with the Active Directory users with an account about to expire. So I created this script for him:

Account with an expiry date.

Account with an expiry date.


$arrUsers = @()
$arrOU    = @("OU=Users,OU=OU1,DC=testdomain,DC=local,DC=lan","OU=Users,OU=OU2,DC=testdomain,DC=local,DC=lan")

ForEach($objOU in $arrOU)
 {
  $arrUsers += (Search-ADAccount -AccountExpiring -SearchBase $objOU -UsersOnly) | Select-Object "AccountExpirationDate","Name",@{Name="OU";Expression={($_."DistinguishedName" -split "=",3)[-1]}},"Enabled","LastLogonDate","LockedOut","ObjectClass","PasswordExpired","PasswordNeverExpires","SamAccountName","UserPrincipalName"
 }
$arrUsers | Sort-Object -Property "OU","Name" | Export-Csv "c:\temp\usersabouttoexpire.csv" -NoTypeInformation

It does what it should do, but not flexible. It can be used in one situation but needs modification for other situations. Also, you cannot specify the number of days when the account is about to expire.

So I created the script AccountsAboutToExpire (v10).ps1.

There are some parameters:

  • OUs: Specify the OUs, seperated by a comma.
  • IncludeChildOUs: Use this switch to include the child OU’s.
  • NumberOfDaysToExpirationDate: Specify the number of days in which the account expires. Default = 7

This script does not only give the accounts that are about to expire, but also the expired user accounts.

A demonstration of this script can be found on my YouTube channel: the script and AppV Repository or view it below:

The script:

<#
.SYNOPSIS
    Gives the accounts that about to expire within the given period.

.DESCRIPTION
    This script gives the accounts that are about to expire within the given period in a CSV file.

.EXAMPLE
    Reports all the accounts that are about to expire in the OUs OU=Users,OU=OU1,DC=testdomain,DC=local,DC=lan and OU=Users,OU=OU2,DC=testdomain,DC=local,DC=lan
    ."\AccountsAboutToExpire V10).ps1" -OUs "OU=Users,OU=OU1,DC=testdomain,DC=local,DC=lan","OU=Users,OU=OU2,DC=testdomain,DC=local,DC=lan"

.EXAMPLE
    Reports all the accounts that are about to expire in the OU DC=testdomain,DC=local,DC=lan, including the child OU's
    ."\AccountsAboutToExpire V10).ps1" -OUs "DC=testdomain,DC=local,DC=lan" -IncludeChildOUs

.EXAMPLE
    Reports all the accounts that are about to expire witin 60 days in the OU DC=testdomain,DC=local,DC=lan, including the child OU's
    ."\AccountsAboutToExpire V10).ps1" -OUs "DC=testdomain,DC=local,DC=lan" -IncludeChildOUs -NumberOfDaysToExpirationDate 60

.NOTES
    Author:  Willem-Jan Vroom
    Website: 
    Twitter: @TheStingPilot

v0.1:
   * Initial version. 

v1.0:
   * Included:
      - All the expired user accounts
      - Sort on expiration date. 
#>

[CmdLetBinding()]

param
(
# Specify the OUs, seperated by a comma. 
[Parameter(Mandatory=$True)]
[string[]] $OUs,

# Use this switch to include the child OU's. 
[Parameter(Mandatory=$False)]
[switch]  $IncludeChildOUs,

# Specify the number of days in which the account expires. Default = 7
[Parameter(Mandatory=$False)]
[string]  $NumberOfDaysToExpirationDate=7
)

# =============================================================================================================================================
# Function block
# =============================================================================================================================================

  Function Get-AccountsAboutToExpire
   {
  
    param
     (
      [string] $OU,
      [Switch] $InclChildOUs,
      [string] $NumDays
     )

    $arrItems       = @()
    $arrAboutToExp  = New-Object PSObject
    $arrExpired     = New-Object PSObject
    $strSearchScope = "OneLevel"

    if($InclChildOUs)
     {
      $strSearchScope = "SubTree"
     } 

    Try
     {
      $arrAboutToExp  = (Search-ADAccount -AccountExpiring -SearchBase $objOU -UsersOnly -SearchScope $strSearchScope -TimeSpan $NumDays) | Select-Object "AccountExpirationDate","Name",@{Name="OU";Expression={($_."DistinguishedName" -split "=",3)[-1]}},@{Name="ExpiredAccount";Expression={$False}},"Enabled","LastLogonDate","LockedOut","PasswordExpired","PasswordNeverExpires"
      $arrExpired     = (Search-ADAccount -AccountExpired  -SearchBase $objOU -UsersOnly -SearchScope $strSearchScope)                    | Select-Object "AccountExpirationDate","Name",@{Name="OU";Expression={($_."DistinguishedName" -split "=",3)[-1]}},@{Name="ExpiredAccount";Expression={$True}}, "Enabled","LastLogonDate","LockedOut","PasswordExpired","PasswordNeverExpires" 
      $arrItems      += $arrAboutToExp
      $arrItems      += $arrExpired
     }
      Catch
     {
      Write-Warning "The OU $OU has not been found."
     }

    Return $arrItems

   }

Function Check-HasAdminRights
 {

  <#
  .NOTES
  ========================================================================================================================
  Created with:     Windows PowerShell ISE
  Created on:       11-January-2019
  Created by:       Willem-Jan Vroom
  Organization:     
  Functionname:     Check-HasAdminRights
  ========================================================================================================================
  .SYNOPSIS

  This function checks if an user has admin rights. The function returns $true or $false

  #>

  If (([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator"))
   {
    Return $True
   }
    else
   {
    Return $False
   }
 }

# =============================================================================================================================================
# End function block
# =============================================================================================================================================

# ========================================================================================================================
# Stop the script for a non admin user
# ========================================================================================================================

  if(-not(Check-HasAdminRights))
  {
   Write-Error "The current user has no admin rights. Please rerun the script with elevated rights." -Category PermissionDenied
   Exit 999
  }

# =============================================================================================================================================
# Check if the module ActiveDirectory has been loaded.
# =============================================================================================================================================

  if(-not(Get-Module -ListAvailable ActiveDirectory))
   {
    Write-Warning "The module ActiveDirectory is not found. Thus quitting."
    Exit 9
   }

# =============================================================================================================================================
# Define the variables
# =============================================================================================================================================

  $arrUsers      = @()
  $strCurrentDir = Split-Path -parent $MyInvocation.MyCommand.Definition
  
# =============================================================================================================================================
# Define the output file
# =============================================================================================================================================

  $strOutputFile = "UserAccounts About to Expire (" + (Get-Date).ToString('G') + ").csv"
  $strOutputFile = $strOutputFile.replace(":","-").Replace("/","-")
  $strOutputFile = $strCurrentDir + "\" + $strOutputFile  

# =============================================================================================================================================
# Process the OUs.
# =============================================================================================================================================

  ForEach($objOU in $OUs)
   {
    if($IncludeChildOUs)
     {
      $arrUsers += Get-AccountsAboutToExpire -OU $objOU -NumDays $NumberOfDaysToExpirationDate -InclChildOUs
     }
      else
     {
      $arrUsers += Get-AccountsAboutToExpire -OU $objOU -NumDays $NumberOfDaysToExpirationDate
     }
   }

# =============================================================================================================================================
# Write the output to a csv file.
# =============================================================================================================================================
  
  $arrUsers | Sort-Object -Property "AccountExpirationDate","OU","Name" | Export-Csv $strOutputFile -NoTypeInformation

Get default help.

Get default help.

 

Get detailed help.

Get detailed help.

 

Output in Excel.

Output in Excel.

 

Current version: AccountsAboutToExpire (v10)

Previous versions:




Migrate users from Windows 7 to Windows 10 automatically

For a migration from Windows 7 to Windows 10 it is needed to do the following with the users’ account:

  • Move it to a different OU
  • Add it to a new VDI group
  • Add it to the new application groups
  • Remove it from the previous application groups
  • Clear the profile path, if needed.

To do this, I created a Powershell script.

The users who need to be migrated from Windows 7 to Windows 10 are in a csv file:

  • The column Userid contains the userid
  • The column NewOU contains the OU where the user should be moved to. This column can be empty.
  • The colum VDIGroup contains the new VDI group. This column can be empty.
  • The colum GroupsToAdd contains the groups the user should be added to. Split the groups with a comma. This column can be empty.
  • The colum GroupsToRemove contains the groups the user should be removed from. Split the groups with a comma. This column can be empty.

An example:

"Userid","NewOU","VDIGroup","GroupsToAdd","GroupsToRemove"
"userid1","OU=Users,OU=OU2,OU=OU1,DC=testdomain,DC=local,DC=lan","Citrix VDI Windows 10","Appl_Group1,Appl_Group2,Appl_Group3","old_appl_group1,old_appl_group2,old_appl_group3,old_appl_group4"
"userid2","OU=Users,OU=OU2,OU=OU1,DC=testdomain,DC=local,DC=lan","Citrix VDI Windows 10","",""
"userid3","OU=Users,OU=OU2,OU=OU1,DC=testdomain,DC=local,DC=lan","","",""
"userid4","","","","old_appl_group1,old_appl_group2"

The are some parameters:

  • FileWithUseridsInCSVFormat: CSV Filename that contains all the userids that should be migrated. If not mentioned than the script name is used.
  • LogFilePrefix: The name the logfile starts with. So the logfiles are grouped together. Default = ZZZ-Logfile_
  • ProductionRun: Use the swith ProductionRun to modify. If not specified, the script is run in test mode.
  • FullCleanUp: Use the swith FullCleanUp to remove all unneeded groups.
  • ClearProfilePath: Use the swith ClearProfilePath to clear the profile path.

The script:

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Filename:         Migrate users (v03).ps1
=============================================================================================================================================
.DESCRIPTION:

This script prepares the users for the migration from Windows 7 to Windows 10.

.USAGE:

Create a CSV file with the following layout:

"Userid","NewOU","VDIGroup","GroupsToAdd","GroupsToRemove"
"userid1","OU=Users,OU=OU2,OU=OU1,DC=testdomain,DC=local,DC=lan","Citrix VDI Windows 10","Appl_Group1,Appl_Group2,Appl_Group3","old_appl_group1,old_appl_group2,old_appl_group3,old_appl_group4"
"userid2","OU=Users,OU=OU2,OU=OU1,DC=testdomain,DC=local,DC=lan","Citrix VDI Windows 10","",""
"userid3","OU=Users,OU=OU2,OU=OU1,DC=testdomain,DC=local,DC=lan","","",""
"userid4","","","","old_appl_group1,old_appl_group2"

.PARAMETERS:

-FileWithUseridsInCSVFormat:
The filename with the users to be modified.

-LogFilePrefix
The name the logfile starts with. So the logfiles are grouped together. Default = ZZZ-Logfile_

-ProductionRun
If the switch is used then ActiveDirectory is modified. If not used then a test run is done, and Active Directory is not modified.

-FullCleanUp
If this switch is used then the following groups will be removed from the users' account:
 * All gg_appl groups
 * WM-Users

 -ClearProfilePath
 If this switch is used then the profile path will be cleared.
  
.VERSION HISTORY:
 v0.1:
   * Initial version.

 v0.2:
   * The logfilename has been changed.

 V0.3:
   * The logfile mentions 'RUNNING IN TEST MODE' in case the swith -ProductionRun is not used.
#>

param
(
[Parameter(HelpMessage="CSV Filename that contains all the userids that should be migrated. Default = the script name, with the csv extension.")]
[String] $FileWithUseridsInCSVFormat="",

[Parameter(HelpMessage="The name the logfile starts with. So the logfiles are grouped together. Default = ZZZ-Logfile_")]
[String] $LogFilePrefix = "ZZZ-Logfile_",

[Parameter(HelpMessage="Use the swith ProductionRun to modify. If not specified, the script is run in test mode.")]
[Switch] $ProductionRun,

[Parameter(HelpMessage="Use the swith FullCleanUp to remove all unneeded groups.")]
[Switch] $FullCleanUp,

[Parameter(HelpMessage="Use the swith ClearProfilePath to clear the profile path.")]
[Switch] $ClearProfilePath
)

# =============================================================================================================================================
# Function block
# =============================================================================================================================================

Function Write-EntryToResultsFile
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       03-August-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Write-EntryToResultsFile
=============================================================================================================================================
.DESCRIPTION:

This function adds the success or failure information to the array that contains the log
information.

#>
param
 (
  $strUserid,
  $ErrorMessage = "",
  $Action       = ""
 )
 $Record            = [ordered] @{"Username" = "";"Action"= "";"Testmode"="";"Error"= ""}
 $Record."Username" = $strUserid
 $Record."Action"   = $Action
 $Record."Testmode" = -not($ProductionRun)
 $Record."Error"    = $ErrorMessage
 $objRecord         = New-Object PSObject -Property $Record
 $Global:arrTable   += $objRecord
}

Function Remove-ProfilePathFromUserProfileInAD
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Remove-ProfilePathFromUserProfileInAD
=============================================================================================================================================
.DESCRIPTION:

This function clears the ProfilePath from AD.

#>

  param
   (
    $strUserid
   )
   Try
      {
       $strUserDN = (Get-ADUser -Identity $strUserid).distinguishedName
       Set-ADUser -Identity $strUserDN -Clear profilePath -WhatIf:(-not($ProductionRun)) -Confirm:$false
       Write-EntryToResultsFile -strUserid $strUserid -Action "Clear profile path"
      }
       Catch
      {
       Write-EntryToResultsFile -strUserid $strUserid -ErrorMessage $_.Exception.Message -Action "Clear profile path"
       Continue
      }
}

Function Move-ADUserToOtherOU
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Move-ADUserToOtherOU
=============================================================================================================================================
.DESCRIPTION:

This function moves the user to another OU.

#>

  param
   (
    $strUserid,
    $strDestinationOU
   )
   Try
     {
      if($strDestinationOU.Length -gt 0)
      {
       $strUserDN = (Get-ADUser -Identity $strUserid).distinguishedName
       Move-ADObject -Identity $strUserDN -TargetPath $strDestinationOU -WhatIf:(-not($ProductionRun)) -Confirm:$false
       Write-EntryToResultsFile -strUserid $strUserid -Action "Move AD User to OU $strDestinationOU."
      }
     }
      Catch
     {
      Write-EntryToResultsFile -strUserid $strUserid -ErrorMessage $_.Exception.Message -Action "Move AD User to OU $strDestinationOU."
      Continue
     }
}

Function Add-ADMemberToGroup
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Add-ADMemberToGroup
=============================================================================================================================================
.DESCRIPTION:

This function adds a user to an AD group.

#>

  param
   (
    $strUserid,
    $strADGroupName
   )
   Try
     {
      if($strADGroupName.Length -gt 0)
      {
       $strUserDN = (Get-ADUser -Identity $strUserid).distinguishedName
       Add-ADGroupMember -Identity $strADGroupName -Members $strUserDN -WhatIf:(-not($ProductionRun)) -Confirm:$false
       Write-EntryToResultsFile -strUserid $strUserid -Action "Add AD group $strADGroupName to user."
      }
     }
      Catch
     {
      Write-EntryToResultsFile -strUserid $strUserid -ErrorMessage $_.Exception.Message -Action "Add AD group $strADGroupName to user."
      Continue
     }
}

Function Remove-ADMemberFromGroup
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Remove-ADMemberFromGroup
=============================================================================================================================================
.DESCRIPTION:

This function removes a user from an AD group.

#>

  param
   (
    $strUserid,
    $strADGroupName
   )
   Try
     {
      if($strADGroupName.Length -gt 0)
      {
       $strUserDN = (Get-ADUser -Identity $strUserid).distinguishedName
       Remove-ADGroupMember -Identity $strADGroupName -Members $strUserDN -WhatIf:(-not($ProductionRun)) -Confirm:$false
       Write-EntryToResultsFile -strUserid $strUserid -Action "Remove AD group $strADGroupName from user."
      }
     }
      Catch
     {
      Write-EntryToResultsFile -strUserid $strUserid -ErrorMessage $_.Exception.Message -Action "Remove AD group $strADGroupName from user."
      Continue
     }
}

# =============================================================================================================================================
# End function block
# =============================================================================================================================================

# =============================================================================================================================================
# Declares the variables.
# =============================================================================================================================================

  $valCounter                        = 1
  $Global:arrTable                   = @()
  $strActivity                       = "Modifying users in Active Directory."
  $arrGroupMustContainForFullCleanUp = @("gg_appl","WM-Users")
  $strCurrentPath                    = Split-Path -parent $MyInvocation.MyCommand.Definition
  $strCurrentFile                    = $MyInvocation.MyCommand.Name


# =============================================================================================================================================
# Define the CSV Import File. 
# =============================================================================================================================================

  if($FileWithUseridsInCSVFormat.Length -eq 0)
   {
    $strCSVFileName = $strCurrentFile -Replace ".ps1",".csv"
   }
    else
   {
    if($FileWithUseridsInCSVFormat.ToLower().IndexOf(".csv") -eq -1)
    {
     $FileWithUseridsInCSVFormat = $FileWithUseridsInCSVFormat + ".csv"
    }
    $strCSVFileName = $FileWithUseridsInCSVFormat
   }

# =============================================================================================================================================
# Check if the string $strCSVFileName is a path. In that case, nothing has to be done.
# In case it is not a path, then the current location should be added.
# =============================================================================================================================================

  if (-not(Split-Path($strCSVFileName)))
  { 
   $strCSVFileName = $strCurrentPath + "\" + $strCSVFileName
  }

# =============================================================================================================================================
# Define the log file. This log file contains all the results.
# =============================================================================================================================================

  $strLastPartOfFileName = " (" + (Get-Date).ToString('F') + ").csv"
  $strLastPartOfFileName = $strLastPartOfFileName -replace ":","-"
  
  If(-not($ProductionRun))
  {
  $strLastPartOfFileName = " (RUNNING IN TEST MODE)" + $strLastPartOfFileName
  }

  $strCSVLogFileSucces   = $strCSVFileName -Replace ".csv", $strLastPartOfFileName

  $strPathName           = (Split-Path $strCSVLogFileSucces) + "\"
  $strFileName           = $strCSVLogFileSucces.Substring($strPathName.Length,($strCSVLogFileSucces.Length - $strPathName.Length)) 

  $strCSVLogFileSucces   = $strPathName + $LogFilePrefix + $strFileName
  
# =============================================================================================================================================
# Read the CSV file.
# =============================================================================================================================================
      
  if(Test-Path $strCSVFileName)
  {
   $arrUserids = @(Import-Csv $strCSVFileName)
  }
   Else
  {
   Write-Host "The import file $strCSVFileName does not exists."
   Exit 1
  }
    
# =============================================================================================================================================
# Process the users in the CSV file.
# =============================================================================================================================================

  Clear-Host

  If(-not($ProductionRun))
  {
  $strActivity = $strActivity + " (RUNNING IN TEST MODE)"
  }

  ForEach($objUser in $arrUserids)
  {
   $strUserid = $objUser.Userid
   Write-Progress -Activity $strActivity -Status "Processing user $strUserid" -PercentComplete ($valCounter / $arrUserids.Count * 100)
   Try
   {
    $strNewOU          = $objUser.NewOU
    $strVDIGroup       = $objUser.VDIGroup
    $arrGroupsToAdd    = $objUser.GroupsToAdd.Split(",")
    $arrGroupsToRemove = $objUser.GroupsToRemove.Split(",")
   }
    Catch
   {
    Write-Host "There is something wrong with the CSV filename $strCSVFileName while processing $strUserid."
    Exit 1 
   }
   
   # Clear the profile path
   If($ClearProfilePath)
   {
    Remove-ProfilePathFromUserProfileInAD -strUserid $strUserid
   }
   
   # Move the user to another OU:
   Move-ADUserToOtherOU -strUserid $strUserid -strDestinationOU $strNewOU
   
   # Add the user to the new VDI group:
   Add-ADMemberToGroup -strUserid $strUserid -strADGroupName $strVDIGroup

   # Add the user to various groups:
   ForEach($objGroupToAdd in $arrGroupsToAdd)
   {
    Add-ADMemberToGroup -strUserid $strUserid -strADGroupName $objGroupToAdd
   }
  
   # Remove the user to various groups:
   ForEach($objGroupToRemove in $arrGroupsToRemove)
   {
    Remove-ADMemberFromGroup -strUserid $strUserid -strADGroupName $objGroupToRemove
   }
  
   # Full Clean Up
   if($FullCleanUp)
   {
    $arrGroups = @(Get-ADUser $strUserid -Properties MemberOf).MemberOf
    forEach($objGroup in $arrGroups)
    {
    $strGroup=(Get-ADGroup $objGroup).Name
    forEach($objGroupMustContain in $arrGroupMustContainForFullCleanUp)
     {
      $strGroupMustContain = $objGroupMustContain.ToString().ToLower()
      if($strGroup.ToLower().IndexOf($strGroupMustContain) -eq 0)
       {
        Remove-ADMemberFromGroup -strUserid $strUserid -strADGroupName $strGroup
       }  
     }
    }
   }

   $valCounter++
   Sleep 2
  }

  Sleep 2

# =============================================================================================================================================
# Write the results to the csv file.
# =============================================================================================================================================
  
  If($Global:arrTable.Count -gt 0)
  {
    $Global:arrTable | Export-Csv $strCSVLogFileSucces -NoTypeInformation
  }
   Else
  {
    Write-Host "Something went wrong while writing the logfile $strCSVLogFileSucces. Maybe nothing to report..."
  } 

Any feedback to improve this script is appreciated. You can download the scripts here:

  1. Migrate users (v01)
  2. Migrate users (v02)
  3. Migrate users (v03)



Inventory the directory access rights on file servers

During the migration, it was needed to inventory the directory access on several file servers. So we could easily monitor the access rights on several directories on the file servers. Some applications are started from a UNC path. So we could add the ‘old’ and the ‘new’ group and check if that has been done properly.

For testing I created the following structure:

\\DEMOATS-SCCM\DEMO.
\---share1
    +---Appl1
    +---Appl2
    |   \---Test
    +---Appl3
    |   +---Sub1
    |   \---Sub2
    |       \---SubSub1
    \---Appl4

I had some challenges:

  • Make a difference between inherited and non-inherited rights. I only want to see the differences. But that can be changed with the parameter showInherited
  • There are some file servers where all the shares (from the root) have to be inventoried. \\server\share did not work, as the rights where inherited. And the script did not see that properly. So I had to inventory the ‘root’ shares on the server. And go through all the directories. I found the code on StackOverflow.
  • And a lot of testing.

The script that I created:

<#
.NOTES
===============================================================================================
Created with:     Windows PowerShell ISE
Created on:       03-August-2018
Created by:       Willem-Jan Vroom
Organization:     
Filename:         Inventory Permissions on Shares (v02).ps1
===============================================================================================
.DESCRIPTION:

This script writes the directory permissions of the given shares to a CSV file.

.USAGE:

1.
Run an inventory on the shares  \\server\share,\\server2\share1 and all shares on \\server3\
with the default setting of a search level of 1 and only show the directories that are not
inherited for the AD group 'Users':
.\"Inventory Permissions on Shares (v02).ps1" -ShareList \\server\share,\\server2\share1,\\server3\

2.
Run an inventory on the share \\server\share with an search level of 10 for all the 'Appl' 
groups:
.\"Inventory Permissions on Shares (v02).ps1" -ShareList \\server\share -NumberOfLevelsToSearch 10 -GroupNameToSearchFor "Appl"

3.
Run a complete inventory for one server for all groups:
.\"Inventory Permissions on Shares (v02).ps1" -ShareList \\server\ -NumberOfLevelsToSearch 10 -GroupNameToSearchFor "" -showInherited

.VERSION HISTORY:
 v0.1:
   * Initial version.

 v.0.2:
   * Option -Outputfile has been added.
   * Added help text by the options.

 v.0.3:
   * The parameter showInherited has become a switch.

#>

param
(
[Parameter(Mandatory=$true,HelpMessage="Please mention the shares you want to inventory regarding the permissions. One name each line.")]
[String[]] $ShareList,

[Parameter(HelpMessage="Give a part of the group name to search for. Leave empty for all groups. Default = Users")]
$GroupNameToSearchFor    = "Users",

[Parameter(HelpMessage="Give the search level. Default = 1")]
$NumberOfLevelsToSearch  = 1,

[Parameter(HelpMessage="Show inherited directories, if specified.")]
[switch]$showInherited,

[Parameter(HelpMessage="Mention the output file. Default is the script name, with csv as the extension.")]
$OutputFile              = ""
)

# ===============================================================================================
# Function block
# ===============================================================================================

Function Get-NetShares
{

<#
.NOTES
===============================================================================================
Created with:     Windows PowerShell ISE
Created on:       03-August-2018
Created by:       https://stackoverflow.com/users/2102693/bill-stewart
Organization:     
Functionname:     Get-NetShares
===============================================================================================
.DESCRIPTION:

This function finds all the shares that are on a server.

I have found this script here:
https://stackoverflow.com/questions/45089582/using-get-childitem-at-root-of-unc-path-servername
(C) by https://stackoverflow.com/users/2102693/bill-stewart

#>

param
(
  [String] $ComputerName = "."
)

Add-Type @"
using System;
using System.Runtime.InteropServices;
using System.Text;
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct SHARE_INFO_1
{
  [MarshalAs(UnmanagedType.LPWStr)]
  public string shi1_netname;
  public uint shi1_type;
  [MarshalAs(UnmanagedType.LPWStr)]
  public string shi1_remark;
}
public static class NetApi32
{
  [DllImport("netapi32.dll", SetLastError = true)]
  public static extern int NetApiBufferFree(IntPtr Buffer);
  [DllImport("netapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
  public static extern int NetShareEnum(
    StringBuilder servername,
    int level,
    ref IntPtr bufptr,
    uint prefmaxlen,
    ref int entriesread,
    ref int totalentries,
    ref int resume_handle);
}
"@

$pBuffer = [IntPtr]::Zero
$entriesRead = $totalEntries = $resumeHandle = 0
$result = [NetApi32]::NetShareEnum(
  $ComputerName,        # servername
  1,                    # level
  [Ref] $pBuffer,       # bufptr
  [UInt32]::MaxValue,   # prefmaxlen
  [Ref] $entriesRead,   # entriesread
  [Ref] $totalEntries,  # totalentries
  [Ref] $resumeHandle   # resumehandle
)
if ( ($result -eq 0) -and ($pBuffer -ne [IntPtr]::Zero) -and ($entriesRead -eq $totalEntries) ) {
  $offset = $pBuffer.ToInt64()
  for ( $i = 0; $i -lt $totalEntries; $i++ ) {
    $pEntry = New-Object IntPtr($offset)
    $shareInfo = [Runtime.InteropServices.Marshal]::PtrToStructure($pEntry, [Type] [SHARE_INFO_1])
    $shareInfo
    $offset += [Runtime.InteropServices.Marshal]::SizeOf($shareInfo)
  }
  [Void] [NetApi32]::NetApiBufferFree($pBuffer)
}
if ( $result -ne 0 ) {
  Write-Error -Exception (New-Object ComponentModel.Win32Exception($result))
}
}

Function Add-EntryToReport
{

<#
.NOTES
===============================================================================================
Created with:     Windows PowerShell ISE
Created on:       03-August-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Add-EntryToReport
===============================================================================================
.DESCRIPTION:

This function adds an entry to the report. When the shares have been searched, the report is
exported to a CSV file.

#>

param
 (
  $FolderNameToAdd,
  $ErrorMessage = "",
  $ADGroup      = "",
  $Permissions  = "",
  $Inherited    = ""
 )
  $Record = [ordered] @{"FolderName" = "";"AD Group" = "";"Permissions" = "";"Inherited" = "";"Error" = ""}
  $Record."FolderName"  = $FolderNameToAdd
  $Record."Error"       = $ErrorMessage
  $Record."AD Group"    = $ADGroup
  $Record."Permissions" = $Permissions
  $Record."Inherited"   = $Inherited
  $Global:Report += New-Object -TypeName PSObject -Property $Record
}


Function Search-InTheFolder
{

<#
.NOTES
===============================================================================================
Created with:     Windows PowerShell ISE
Created on:       03-August-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Search-InTheFolder
===============================================================================================
.DESCRIPTION:

This function goes through all the folders in the given location.

The variable $numLevels gives the number of levels that the search goes. The less the number, 
the quicklier the script is.

#>

param
(
  $RootOfTheShare,
  $numLevels = 1
)

$valNumberOfDirectories = 0 
$valCounterOfDirectores = 0

Try
  {
$FolderPath = Get-ChildItem -Path $RootOfTheShare -Directory -Recurse -Force -Depth $numLevels -ErrorAction SilentlyContinue
Write-Progress -Activity "Counting the number of folders in the share." -Id 2 -ParentId 1
Foreach ($Folder in $FolderPath)
     {
      $valNumberOfDirectories++
     }
  
   Foreach ($Folder in $FolderPath)
     {
      $FolderFullName = $Folder.FullName
      Write-Progress -Activity "Going through all the shares." -Status "Processing share $FolderFullName." -Id 2 -PercentComplete ($valCounterOfDirectores / $valNumberOfDirectories * 100) -ParentId 1
      Get-FolderRights -FolderNameToInvestigate $Folder.FullName
      $valCounterOfDirectores++
     }
  }
  Catch
  {
  Add-EntryToReport -FolderNameToAdd $RootOfTheShare -ErrorMessage $_.Exception.Message
  }

}


Function Get-FolderRights
{

<#
.NOTES
===============================================================================================
Created with:     Windows PowerShell ISE
Created on:       03-August-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Get-FolderRights
===============================================================================================
.DESCRIPTION:

This function puts the access information in an array.
If the parameter GroupNameToSearchFor is empty or "" then all the groups are shown.

#>
Param($FolderNameToInvestigate)
$GroupNameToSearchFor = $GroupNameToSearchFor.ToLower()
Try
    {
    $Acl = Get-Acl -Path $FolderNameToInvestigate -ErrorAction SilentlyContinue
    foreach ($Access in $acl.Access)
        {           
              $Group=$Access.IdentityReference
              if ($GroupNameToSearchFor.Length -ge 1)
               {
                $Position = $Group.ToString().ToLower().IndexOf($GroupNameToSearchFor)
               }
                Else
               {
                $Position = 2
               }
              if($Position -ge 1)
              {
               [bool]$blnIsInheriated = $Access.IsInherited
               if([bool]$showInherited)
                {
                 Add-EntryToReport -FolderNameToAdd $FolderNameToInvestigate -ADGroup $Access.IdentityReference -Permissions $Access.FileSystemRights -Inherited [bool]$blnIsInheriated
                }
               if((-not[bool]$showInherited) -and (-not[bool]$blnIsInheriated))
                {
                 Add-EntryToReport -FolderNameToAdd $FolderNameToInvestigate -ADGroup $Access.IdentityReference -Permissions $Access.FileSystemRights -Inherited [bool]$blnIsInheriated
                }
              }
        }
        }
        Catch
        {
        Add-EntryToReport -FolderNameToAdd $FolderNameToInvestigate -ErrorMessage $_.Exception.Message
        Continue
        }
}

# ===============================================================================================
# End function block
# ===============================================================================================

# ===============================================================================================
# Define the CSV Export File. 
# ===============================================================================================

  $currentPath                    = Split-Path -parent $MyInvocation.MyCommand.Definition
  $strCurrentFile                 = $MyInvocation.MyCommand.Name
  
  if($OutputFile.Length -eq 0)
   {
    $strCSVFileName                 = $strCurrentFile -Replace ".ps1",".csv"
   }
    else
   {
    if($OutputFile.ToLower().IndexOf(".csv") -eq -1)
    {
     $OutputFile = $OutputFile + ".csv"
    }
    $strCSVFileName = $OutputFile
   }

# ===============================================================================================
# Check if the string $strCSVFileName is a path. In that case, nothing has to be done.
# In case it is not a path, then the current location should be added.
# ===============================================================================================


  if (Split-Path($strCSVFileName))
  {
   $CSVExportFile = $strCSVFileName
  }
   else
  { 
   $CSVExportFile = $currentPath + "\" + $strCSVFileName
  }

# ===============================================================================================
# Create the folder as a part of $CSVExportFile if not exists.
# ===============================================================================================

  $PathFromCSVExportFile = Split-Path $CSVExportFile
  if(-not(Test-Path(Split-Path $PathFrom$CSVExportFile)))
  {
   New-Item -Path $PathFromCSVExportFile -ItemType Directory
  }

# ===============================================================================================
# Define variables.
# ===============================================================================================

  $arrShares                      = @($ShareList)
  $Global:Report                  = @()
    
# ===============================================================================================
# Start the job.
# ===============================================================================================

  Clear-Host
  Try
     {
      Import-Module ActiveDirectory
     }
     Catch
     {
     Write-Host The module ActiveDirectory could not be loaded.
     Exit 1
     }
  Write-Host (Get-Date).ToString('T') " Starting..."

# ===============================================================================================
# Deletes the CSV file if exists.
# ===============================================================================================

  If(Test-Path $CSVExportFile)
  {
  Remove-Item $CSVExportFile
  }

# ===============================================================================================
# Go through all the shares as defined in the array $arrShares
# ===============================================================================================
  
  $valCounter       = 1 
  $valNumerOfShares = $arrShares.Count 
  ForEach ($shareName in $arrShares)
  {
   Write-Progress -Id 1 -Activity "Going through the shares" -Status "Checking share $shareName ($valCounter of $valNumerOfShares)" -PercentComplete ($valCounter / $valNumerOfShares*100)
   
   $LastCharacter = $shareName.SubString($shareName.Length-1,1)

   # ===============================================================================================
   # If the last character is not a '\' then it is a regular share. Then it is simple: call the
   # function 'Search-InTheFolder'
   #
   # If the last character is a '\' then only the servername is given. So first find all the
   # shares on that server. After that, process all the subshares.
   # ===============================================================================================
   
   if ($LastCharacter -ne "\")
   {
   Search-InTheFolder -RootOfTheShare $shareName -numLevels $NumberOfLevelsToSearch
   }
   Else
   {
     $arrShares = Get-NetShares -ComputerName $shareName
     ForEach($objShare in $arrShares)
     {
      $LastCharacter = ($objShare.shi1_netname).SubString(($objShare.shi1_netname).Length-1,1)
      if($LastCharacter -ne "$")
      {
       
       # ===============================================================================================
       # Ignore C$, D$, IPC$, NETADMIN$ etc.
       # This means that all the hidden shares are ignored.
       # ===============================================================================================
             
       $ServerShareName = $shareName + $objShare.shi1_netname
       Get-FolderRights -FolderNameToInvestigate $ServerShareName
       Search-InTheFolder -RootOfTheShare $ServerShareName -numLevels $NumberOfLevelsToSearch
      }
     }  
   }
   $valCounter++
  }

# ===============================================================================================
# Output naar een CSV file
# ===============================================================================================

  $Global:Report | Sort-Object -Property FolderName,"AD Group" | Export-Csv -path $CSVExportFile -NoTypeInformation -Encoding ASCII
  Write-Host You can open the file $CSVExportFile now. 
  Write-Host (Get-Date).ToString('T')  " Ended..." 

Any feedback to improve this script is appreciated. You can download the scripts here:

  1. Link to Inventory Permissions on Shares (v0.1)
  2. Link to Inventory Permissions on Shares (v0.2)
  3. Link to Inventory Permissions on Shares (v0.3)



Cannot edit the object which is in use as SCCM blocks an application

You might have had this earlier: editing an application in SCCM 2012, the SCCM console crashes. And when you want to continue, you get the error: The following objects are not available and wil not be edited.

In this article, I will describe 2 solutions.

Via PowerShell

  1. The easiest way is via PowerShell. In the SCCM Console start PowerShell:

  2. Give the following command:
    Unlock-CMObject -InputObject $(Get-CMApplication -Name Microsoft_CPlusPlusRuntimes_1.0.1_ENG_WIN7_P1)
    


    There is no feedback, but you can edit the application again.

Via SQL Server Management Studio

An alternative way is via SQL Server Management Studio.

  1. Start SQL Server Management Studio
  2. Create a new query. And connect to the correct database.
  3. Give the following command:
    select * from SEDO_LockState where LockStateID<>0
    

  4. There might be multiple lines, as multiple users may have various applications locked. Find the correct one. Copy the LockstateID.
  5. Give the following command:
    DELETE from SEDO_LockState where LockID = 'Copied LockstateID'
    

  6. And you can edit your application again.

 




Check if there is a working internet connection when a firewall blocks ping requests

Sometimes you need to check if there is a working internet connection. You can use the
pingcommand for that. But this command does not work if ICMP echo request have disabled on the firewall. If this is the case we have to check the status of a website. If the status is 200 then there is a working internet connection.

In this example I will use both vbscript and powershell to check if there is a working internet connection.

The basics

  1. The command Invoke-WebRequest -Uri www.google.com -TimeoutSec 1 -UseBasicParsing gives a lot of internet about the webpage. Pay attention to the statuscode.

    Invoke-WebRequest -Uri www.google.com -TimeoutSec 1 -UseBasicParsing

  2. The command (Invoke-WebRequest -Uri www.google.com -TimeoutSec 1 -UseBasicParsing).StatusCode returns only the statuscode.

    (Invoke-WebRequest -Uri www.google.com -TimeoutSec 1 -UseBasicParsing).StatusCode

  3. There is an error message if there is no working internet connection.

    Invoke-WebRequest -Uri www.google.com -TimeoutSec 1 -UseBasicParsing
    With no working internet connection.

  4. If there is a working internet connection then the script returns 200.

    Try {(Invoke-WebRequest -Uri www.google.com -TimeoutSec 1 -UseBasicParsing).StatusCode} Catch {Write-Host “Error!!!”}
    Working internet connection.

  5. In this case there is no working internet connection, so an error message is shown.

    Try {(Invoke-WebRequest -Uri www.google.com -TimeoutSec 1 -UseBasicParsing).StatusCode} Catch {Write-Host “Error!!!”}
    No working internet connection.

Further testing with PowerShell

<#
.NOTES
===========================================================================
Created with:     Windows PowerShell ISE
Created on:       21-October-2017
Created by:       Willem-Jan Vroom
Organization:     
Filename:         CheckIfWebsiteIsReachable.ps1
===========================================================================
.DESCRIPTION:

It checks if there is a working internet connection by reading the webpage.
This is very usefull in case the ping command does not work when ICMP echo
requests are disabled on the firewall.

#>
param ($URL)
Function Get-StatusCodeFromWebsite
{
  param($Website)
  Try
   {
    (Invoke-WebRequest -Uri $Website -TimeoutSec 1 -UseBasicParsing).StatusCode
   }
  Catch
   {
   }
}

$URL = "www.google.com"
$Result = Get-StatusCodeFromWebsite -Website $URL
if($Result -eq 200)
 {
  Write-Host "The website $URL is reachable."
 }
 else
 {
  Write-Host "The website $URL is not reachable."
 }

Result if there is a working internet connection.

Result if there is no working internet connection.

The next step: use vbscript and powershell to detect if there is a working internet connection

As far as I know vbscript has not the option to detect the status from a website. So we use the powershell function with vbscript to detect if there is a working internet connection.

The powershell script has changed a little bit:

<#
.NOTES
===========================================================================
Created with:     Windows PowerShell ISE
Created on:       21-October-2017
Created by:       Willem-Jan Vroom
Organization:     
Filename:         CheckIfWebsiteIsReachable_with_vbscript.ps1
===========================================================================
.DESCRIPTION:

It checks if there is a working internet connection by reading the webpage.
This is very usefull in case the ping command does not work when ICMP echo
requests are disabled on the firewall.

#>
Param ($URL)
Function Get-StatusCodeFromWebsite
{
  param($Website)
  Try
   {
    (Invoke-WebRequest -Uri $Website -TimeoutSec 1 -UseBasicParsing).StatusCode
   }
  Catch
   {
   }
}

$Result = Get-StatusCodeFromWebsite -Website $URL
if($Result -eq 200)
 {
  Exit 0
 }
 else
 {
  Exit 1
 }

It exits with an exit code:
0 = success
1 = failure

And we use vbscript to check:

' ================================================================================================
' Check if a there is a working network connection. Do not use ping, as the ping command cannot
' bu used in case ICMP echo requests have been disabled on the firewall.
' Created by Willem-Jan Vroom
' Version history:
'
' 0.0.1
'  - Initial version
'
' 1.0.0
'  - Final version
' ================================================================================================

' ------------------------------------------------------------------------------------------------
' Declare the most variables. 
' ------------------------------------------------------------------------------------------------
 
  Option Explicit

  Dim objShell      : set objShell   = WScript.CreateObject("WScript.Shell")
  Dim strWebsite    : strWebsite     = "http://www.vroom.cc"
  Dim strCurrentDir : strCurrentDir  = Left(Wscript.ScriptFullname, InstrRev(Wscript.ScriptFullname, "\"))
  Dim strCommand    : strCommand     = "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -File " & chr(34) & strCurrentDir & "CheckIfWebsiteIsReachable_with_vbscript.ps1" & chr(34) & " -URL "& strWebsite
  Dim strText       : strText        = ""
  Dim valResult     : valResult      = 0

' ------------------------------------------------------------------------------------------------
' Check the website that is mentioned in the string strWebsite. 
' Use the exitcode from the powershell script:
'  0 = Success
'  1 = Failure
' ------------------------------------------------------------------------------------------------

  valResult = objShell.Run(strCommand, 6, True)
  if valResult = 0 Then 
     strText = "The website '" & strWebSite & "' could be reached"
       else
     strText = "The website '" & strWebSite & "' could not be reached"
  end if

' ------------------------------------------------------------------------------------------------
' Display a message box with the result. 
' ------------------------------------------------------------------------------------------------

  msgbox strText,0,"The result."

In this example we try the webpage http://www.vroom.cc/.

Result if there is a working internet connection.

Result if there is no working internet connection.

You can download the scripts in a ZIP file.




Export lines that contain a specific value to a CSV file with PowerShell

For a client I needed to find all the hostnames that where defined in each IBM iSeries Client Access Workstation file (.ws file). All the .ws files where in subfolders. The country, the filename and the hostname needs to be in a .csv file.

The directory structure:

Folder PATH listing for volume Windows
Volume serial number is 000000BE 
C:.
|   
\---Sessions
    +---CAT Austria
    |       CAT_01.WS
    |       CAT_02.WS
    |       
    +---CBE Belgium
    |       CBE_01.ws
    |       CBE_02.ws
    |       
    +---CCH Switzerland
    |       CCH_01.ws
    |       CCH_02.ws
    |       CCH_03.ws
    |       
    \---Generic sessions
            GEN_01_xx.ws
            GEN_02_xx.WS
            GEN_03_xx.ws
            GEN_TEST_xx.WS
            GEN_UAT.WS

And the content of a .ws file:

[Profile]
ID=WS
[Communication]
HostName=HOST653.company.local

As I want to learn PowerShell, I decided to use PowerShell.

Here is my script:

###########################################################
# FILE:     GetHostnamesFromWSFiles_v100.ps1   
#
# AUTHOR:   Willem-Jan Vroom
# DATE:     17-March-2017
# COMMENT:  This script reads all the hostnames as defined
#           in the .ws files
###########################################################

# ---------------------------------------------------------
# Define the variables
# ---------------------------------------------------------

$Directory        = "$PSScriptRoot\*"
$CSVFile          = "$PSScriptRoot\Results.csv"
$arrWithWSFiles   = Get-ChildItem -Path $Directory -Include *.ws -Recurse
$arrResults       = @()

# ---------------------------------------------------------
# Process each .ws file and search for hostname
# If found, add to the array $arrResults:
#    Country:  The foldername that contains the .ws file
#    Filename: The filename from the .ws file
#    Hostname: The entry that begins with hostname= and
#              contains the AS400 host name to connect to.
# ---------------------------------------------------------

ForEach ($WSFileName in $arrWithWSFiles)
{
 $content = Get-Content $WSFileName
 ForEach ($line in $content)
  {
  $line=$line.tolower()
  $Position = $line.IndexOf("hostname")
  if ($Position -eq 0)
     {
     $DetailedLine               = "" | Select Country,FileName,AS400Hostname
     $CountryCode                = ""
     $CountryCode                = $WSFileName.DirectoryName
     $LastPosition               = $CountryCode.LastIndexOfAny("\")
     $DetailedLine.Country       = $CountryCode.SubString($LastPosition+1, $CountryCode.Length - ($LastPosition + 1))
     $DetailedLine.FileName      = $WSFileName.Name
     $DetailedLine.AS400Hostname = $line -replace("hostname=","")
     $arrResults += $DetailedLine     
     } 
  } 
}

# ---------------------------------------------------------
# Export the array with the results to a CSV File.
# ---------------------------------------------------------
 
$arrResults | Export-CSV -Path $CSVFile

And in this case the output looks like:

#TYPE Selected.System.String
"Country","FileName","AS400Hostname"
"CAT Austria","CAT_01.WS","host653.company.local"
"CAT Austria","CAT_02.WS","host007.company.local"
"CBE Belgium","CBE_01.ws","host123.company.local"
"CBE Belgium","CBE_02.ws","host481.company.local"
"CCH Switzerland","CCH_01.ws","host053.company.local"
"CCH Switzerland","CCH_02.ws","host613.company.local"
"CCH Switzerland","CCH_03.ws","host653.company.local"
"Generic sessions","GEN_01_xx.ws","host001.company.local"
"Generic sessions","GEN_02_xx.WS","host002.company.local"
"Generic sessions","GEN_03_xx.ws","host003.company.local"
"Generic sessions","GEN_TEST_xx.WS","hosttest.company.local"
"Generic sessions","GEN_UAT.WS","hostuat.company.local"

The script can be found here.




Deployment Visio 2013 and Project 2013 as AppV 5.0 package created with Click to Run

Office 2013 – and its components – can only be made available as an App-V 5.0 package with Click to Run. The required steps – including streaming to the client with the App-V 5.0 Management Console – are described in this article.

Step 1: Download Office Deployment Tool for Click to Run

  1. Download Office Deployment Tool for Click-to-Run.
  2. Run the downloaded ‘officedeploymenttool.exe’. It will extract the setup.exe together with an example xml file.

Step 2: make the download.xml file

  1. This file contains all the settings which applications should be downloaded. More information can be found at Reference for Click-to-Run configuration.xml file.

    In this example the content is:

    
    
     
        
          
          
          
        
        
          
          
          
        
      
    
    

    [warning]Of course the share should be present.[/warning]
    In this example Visio and Project will be downloaded in English, Czech and Dutch.

  2. Create a batch file with the following content:
    "%~dp0setup.exe" /download "%~dp0download.xml"
    pause
    
  3. Run the script to download the files:
    Run 'download.cmd' as admin.

    Run ‘download.cmd’ as admin.

  4. After a while all the files are downloaded:
    All files have been downloaded.

    All files have been downloaded.

Step 3: make the package.xml files

For both Visio and Project a package xml file will be made. That is needed as Visio and Project have a different package id. That package id is defined in the property PACKAGEGUID in the packager.xml file.

  1. The file for Visio:
    
     
        
          
          
          
        
      
    
      
      
      
      
      
      
    
    
  2. The file for Project:
    
     
        
          
          
          
        
      
    
      
      
      
      
      
      
    
    
  3. The package guids can be created with this powershell command:
    Powershell command: [guid]::newguid()

    Powershell command: [guid]::newguid()

  4. Create the packager.cmd to create the packages for both Visio and Project:
    "%~dp0setup.exe" /packager "%~dp0packager_visio.xml" %~dp0AppV50Packages
    "%~dp0setup.exe" /packager "%~dp0packager_project.xml" %~dp0AppV50Packages
    pause
    
  5. Run the script to create the packages:
    07 Run packager cmd as administrator7
  6. You can view the progress:
    View progress.

    View progress.

  7. Ready:
    10 Finished
  8. If you open ‘ProjectStdRetail_cs-cz_en-us_nl-nl_x86_DeploymentConfig.xml’ there are references to the Czech language:
    11 The deployment config contains references to cz

Step 4: Modify the packages in the sequencer

If needed you can modify the packages in the sequencer. At least App-V 5.0 SP2 sequencer is needed.

In this scenario the packages are modified to change the names.

  1. Open the project .appv file with the sequencer:
    Open the project appv file with the sequencer.

    Open the project appv file with the sequencer.


    Edit package.

    Edit package.

  2. Edit:
    14 Edit
  3. The final result. [important]Please be aware that the first language that is mentioned, is ‘cs-cz’ for Czech. That is why all the shortcuts have a Czech description. I do not know if this is by design or that is a bug…
    You should expect that en-en should be default, as that language is set as the first language in the ‘packager_project.xml’ file.
    15 The results cz as mentioned first[/important]
  4. Modify both the package name and description:
    16 Modify package name and description
  5. And save the package:
    17 Save the package
    18 Check the file name
    19 Wait

Step 5: Import Visio and Project

  1. Import Project 2013 in AppV 5.0 using the Application Virtualization Console
  2. Edit the AD access:
    20 Edit AD access
  3. Unselect the applications you do not need:
    24 Unselect all the applications you do not need
  4. Publish the application
  5. And do the same for Visio (not documented)

Step 6: Test on the client

  1. Log in on a client.
  2. After a short while both Visio and Project are visible in the Start Menu:
    25 After a while the applications are available on the client
  3. Start Word:
    25 After a while the applications are available on the client
  4. You have the option to insert both a Project and Visio object in Word:
    27 Option to insert Project document
    28 Option to insert Visio drawing
  5. And work with it:
    29 Visio

Step 7: Remove Project and Visio

  • Remove the group from the application in the Application Virtualization Console.
  • It might take a longer time if the application is in use:
    30 Visio is still in use
    You see that in the event viewer.

    You see that in the event viewer.


    But Visio is removed after reboot.

    But Visio is removed after reboot.

  • If you start Word, the options for inserting a Project or Visio object are not available anymore:
    33 Options to insert Project and Visio objects are gone



  • User Environment Variables in App-V 5.0 with SP1, SP2 and SP2 Hotfix 4

    Dan Gough wrote an excellent article named: User Environment Variables in App-V 5 Scripts. To summarize: it is about the fact that environment variables are not shown correctly in AppV 5.0.
    However: the question is: is this still the case? Time to investigate!

    To test this, I re-used an old package: Firefox 24. It does not need any startup-scripts, but it is an ideal candidate for testing.

    I have done the following:

    1. I created a VB script with the following content:
      ' ===========================================================================
      ' Example of environment variables when running as a script.
      ' ===========================================================================
      
        Option Explicit
        
        Dim objShell             : set objShell        = CreateObject("WScript.Shell")
        Dim strText              : strText             = ""
        Dim strUserShellFolders  : strUserShellFolders = "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders"
        Dim strShellFolders      : strShellFolders     = "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
        Dim strVolatileEnv       : strVolatileEnv      = "HKCU\Volatile Environment"
        
        strText = strText & "Displaying the various variables when running scripts in AppV 5.0." & vbCrlf
        On Error Resume Next
        strText = strText & "AppV 5.0 Client version:   " & objShell.RegRead("HKLM\SOFTWARE\Microsoft\AppV\Client\Version")
        On Error Goto 0
        strText = strText & vbCrlf & vbCrlf
        
        
        ' Start with environment variables
        strText = strText & "Environment variables:" & vbCrlf
        strText = strText & " - APPDATA:       " & objShell.ExpandEnvironmentStrings("%APPDATA%") & vbCrlf
        strText = strText & " - LOCALAPPDATA:  " & objShell.ExpandEnvironmentStrings("%LOCALAPPDATA%") & vbCrlf
        strText = strText & " - USERNAME:      " & objShell.ExpandEnvironmentStrings("%USERNAME%") & vbCrlf
        strText = strText & " - USERPROFILE:   " & objShell.ExpandEnvironmentStrings("%USERPROFILE%") & vbCrlf & vbCrlf
        
        ' Use HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders
        strText = strText & "Use " & strUserShellFolders & ":" & vbCrlf
        strText = strText & " - AppData:       " & objShell.RegRead(strUserShellFolders & "\Appdata") & vbCrlf
        strText = strText & " - Local AppData: " & objShell.RegRead(strUserShellFolders & "\Local Appdata") & vbCrlf
        strText = strText & " - Desktop:       " & objShell.RegRead(strUserShellFolders & "\Desktop") & vbCrlf & vbCrlf
        
        ' Use HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders
        strText = strText & "Use " & strShellFolders & ":" & vbCrlf
        strText = strText & " - AppData:       " & objShell.RegRead(strShellFolders & "\Appdata") & vbCrlf
        strText = strText & " - Local AppData: " & objShell.RegRead(strShellFolders & "\Local Appdata") & vbCrlf
        strText = strText & " - Desktop:       " & objShell.RegRead(strShellFolders & "\Desktop") & vbCrlf & vbCrlf
        
        ' Use HKEY_CURRENT_USER\Volatile Environment
        strText = strText & "Use " & strVolatileEnv & ":" & vbCrlf
        strText = strText & " - APPDATA:       " & objShell.RegRead(strVolatileEnv & "\APPDATA") & vbCrlf
        strText = strText & " - LOCALAPPDATA:  " & objShell.RegRead(strVolatileEnv & "\LOCALAPPDATA") & vbCrlf
        strText = strText & " - USERNAME:      " & objShell.RegRead(strVolatileEnv & "\USERNAME") & vbCrlf
        strText = strText & " - USERPROFILE:   " & objShell.RegRead(strVolatileEnv & "\USERPROFILE") & vbCrlf
        
        msgbox strText,,"Results from environment variables"
      
    2. Modified the package by adding the file in the scripts folder:
      Add the vbscript in sequencer

      Add the vbscript in sequencer

    3. Installed AppV 5.0 with SP1 on a client
    4. Logged in with an account with standard user rights.
    5. Started PowerShell as admin and installed Firefox. Also I enabled packaging scripts:
      Powershell commands to add the package.

      Powershell commands to add the package.

    6. Started Firefox:
      The environment variables are shown incorrect,

      The environment variables are shown incorrect,

    7. Upgraded the client to SP2:
      Install the client.

      Install the client.


      Reboot the computer.

      Reboot the computer.

    8. Started Firefox:
      The environment variables are shown correctly,

      The environment variables are shown correctly,

    9. Upgraded the client to SP2 with HF4:
      Install the client.

      Install the client.


      Reboot the computer.

      Reboot the computer.

    10. Started Firefox:
      The environment variables are shown correctly,

      The environment variables are shown correctly,

    To summarize: if you want to use scripts and environment variables are needed, then use at least SP2. If you cannot upgrade the client, then use the HKCU\Volatile Environment.




    Installing a MSI package during App-V 5.0 Package deployment

    Driver packages cannot be virtualized as they have to communicate directly with the hardware. So packages using a driver have never been virtualized. Until now.

    With AppV 5.0 it is possible to install the driver while adding an AppV 5.0 package and to uninstall it during the removal of the AppV 5.0 package.

    In this article I will show how this is done. I will use an existing package that I made earlier.

    For this example I made a empty MSI package (it only contains one feature and one component) and that MSI package is installed when the AppV package is added. The details are:

    The properties of the empty MSI file edited with Orca.

    The properties of the empty MSI file edited with Orca.

    1. Add the MSI file to the virtual application

    Ths MSI file is added to the ‘Scripts’ section of the virtual application.

    • Copy the virtual package to a local drive, for example C:\AppV50Demo.
    • Open the AppV 5.0 Package.
      Open AppV Package

      Open AppV Package

    • Select ‘Edit’ to modify the virtual package.
      Select Edit

      Select Edit

    • Click [Edit]
      Click Edit.

      Click Edit.

    • Click on the tab [Package Files].
      Tab Package Files

      Tab Package Files

    • Find and select the folder ‘Scripts’.
      Click [Add].
      Add a script to the AppV 5.0 package.

      Add a script to the AppV 5.0 package.

    • Click [Browse].
      Browse

      Browse

    • Find and select the MSI file. If needed, add all the needed files, like transform and cab files.
      Select the MSI that has to be added to the AppV 5.0 package.

      Select the MSI that has to be added to the AppV 5.0 package.

    • Click [Open].
      Click Open.

      Click Open.

    • Click [Ok].
      Click Ok

      Click Ok

    • The result.
      The result.

      The result.

    • Save the package.
      Save the package

      Save the package

    • Exit the sequencer.
      Exit the sequencer.

      Exit the sequencer.

    2. Edit the deployment xml file

    • Make a copy of the file ‘IgorPavlov_7Zip_9.20_ENG_2_3_4_DeploymentConfig.xml’
    • Rename that copy to ‘IgorPavlov_7Zip_9.20_ENG_2_3_4_DeploymentConfig with MSI.xml’. All the changes will be in this file.
    • Open the file ‘IgorPavlov_7Zip_9.20_ENG_2_3_4_DeploymentConfig with MSI.xml’.
    • Search for the following text:
          
          
    • And replace it by:
          
          
            
      
            
              msiexec.exe
              /i TestMSI.MSI /qb! /l*v c:\windows\system32\LogFiles\Install_TestMSI.log
              
            
            
              msiexec.exe
              /x TestMSI.MSI /qb! /l*v c:\windows\system32\LogFiles\Uninstall_TestMSI.log
              
            
          
      
    • Save the modified ‘IgorPavlov_7Zip_9.20_ENG_2_3_4_DeploymentConfig with MSI.xml’.

    3. Add and publish the virtual application.

    • Copy the AppV 5.0 to a local drive, for example: C:\IgorPavlov_7Zip_9.20_ENG\1.00.00
    • Start PowerShell 3.0 as an administrator.
    • Change the security policy:
      Set-ExecutionPolicy -ExecutionPolicy Unrestricted
    • Import all the AppV client cmdlets:
      Import-Module AppVClient
    • check if it is possible to run scripts. It is needed to change the policy ‘EnablePackageScripts’ to 1.
      Get-AppvclientConfiguration
    • And change the policy if needed:
      Set-AppvclientConfiguration -EnablePackageScripts 1
    • Add and publish the virtual application:
      Add-AppvclientPackage -Path C:\IgorPavlov_7Zip_9.20_ENG\1.00.00\IgorPavlov_7Zip_9.20_ENG_2_3_4.appv -DynamicDeploymentConfiguration "C:\IgorPavlov_7Zip_9.20_ENG\1.00.00\IgorPavlov_7Zip_9.20_ENG_2_3_4_DeploymentConfig with MSI.xml" | Publish-AppVClientPackage -Global
    • Check the logfile in C:\Windows\System32\LogFiles.
    • You will not find the application within ‘Programs and features’.
      Not in Programs and features.

      Not in Programs and features.

    • However, you can find it in the registry.
      You will find the application via regedit.

      You will find the application via regedit.

    4. Remove the virtual application.

    • To uninstall the virtual application:
      Remove-AppVClientPackage -Name *Pavlov*
    • Check the logfile in C:\Windows\System32\LogFiles.



    Using connection groups to interact locally installed applications with virtual applications

    Let’s take the following scnenario: both Microsoft Visio 2010 and Microsoft Project 2010 have been virtualized using Application Virtualization v5.0 techniques. Microsoft Word 2010 has been installed locally. With Microsoft Application Virtualization prior to v5.0 it was not possible to use a virtualized application within a locally installed one. Unless you started the locally installed application within the same bubble as the virtualized application… Too much of a hassle.

    In this example I will demonstrate how to use virtualized applications within a locally installed one.

    Preparation: sequencing

    • Create a sequencing machine with Word 2010.
    • Start Word so there will be no more MSI Self Repairs
    • Install AppV 5.0 sequencer
    • Sequence both Visio 2010 and Project 2010.
      Make sure that the machine is reverted to a clean state before sequencing the second application.

    Actions on the client

    • Make sure that the Application Virtualization Client 5.0 has been installed.
    • Start Powershell (x64) as an administator.
      Enter the command:

      Set-ExecutionPolicy -ExecutionPolicy Unrestricted

      Set-ExecutionPolicy -ExecutionPolicy Unrestricted

      Set-ExecutionPolicy -ExecutionPolicy Unrestricted

      Press ‘y’ to continue

    • Enter the command:
      Import-Module AppVClient

      Import-Module AppVClient

      Import-Module AppVClient

    • Add the virtualized Project 2010 with the command:
      Add-AppVClientPackage -Path "C:\AppV50\Microsoft_Project-AppV50_2010STD_MLI\Microsoft_Project-AppV50_2010STD_MLI_2.appv" | Publish-AppVClientPackage -Global

      Add-AppVClientPackage -Path "C:\AppV50\Microsoft_Project-AppV50_2010STD_MLI\Microsoft_Project-AppV50_2010STD_MLI_2.appv" | Publish-AppVClientPackage -Global

      Add-AppVClientPackage -Path “C:\AppV50\Microsoft_Project-AppV50_2010STD_MLI\Microsoft_Project-AppV50_2010STD_MLI_2.appv” | Publish-AppVClientPackage -Global

      Add-AppVClientPackage -Path "C:\AppV50\Microsoft_Project-AppV50_2010STD_MLI\Microsoft_Project-AppV50_2010STD_MLI_2.appv" | Publish-AppVClientPackage -Global  (Done)

      Add-AppVClientPackage -Path “C:\AppV50\Microsoft_Project-AppV50_2010STD_MLI\Microsoft_Project-AppV50_2010STD_MLI_2.appv” | Publish-AppVClientPackage -Global (Done)

    • Add the virtualized Visio 2010 with the command:
      Add-AppVClientPackage -Path "C:\AppV50\Microsoft_Visio-AppV50_2010STD_MLI\Microsoft_Visio-AppV50_2010STD_MLI_2_3.appv" | Publish-AppVClientPackage -Global

      Add-AppVClientPackage -Path "C:\AppV50\Microsoft_Visio-AppV50_2010STD_MLI\Microsoft_Visio-AppV50_2010STD_MLI_2_3.appv" | Publish-AppVClientPackage -Global

      Add-AppVClientPackage -Path “C:\AppV50\Microsoft_Visio-AppV50_2010STD_MLI\Microsoft_Visio-AppV50_2010STD_MLI_2_3.appv” | Publish-AppVClientPackage -Global

    • You will also find the results in the Event Viewer:

      Eventviewer

      Eventviewer

    • If you start the locally installed Word, you will not see the virtualized Visio 2010 and Project 2010:

      No Visio 2010 and Project 2010 in Word 2010.

      No Visio 2010 and Project 2010 in Word 2010.

    • To enable interaction between the locally installed application and the virtualized application the registry needs to be modified:
    • Start regedit and go to HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\AppV\Client\RunVirtual
    • Create the key ‘winword’.
    • Under the (Default) value, add both the PackageID and VerionId of Visio. There is an underscore between the PackageId and VersionId.

      HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\AppV\Client\RunVirtual

      HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\AppV\Client\RunVirtual

    • Use the following command to find both the PackageId and VersionId:
      Get-AppVClientPackage -Name *Visio*

      Get-AppVClientPackage -Name *Visio*

      Get-AppVClientPackage -Name *Visio*

    • If Word 2010 is started, then go to ‘Insert’ and select ‘Object’ from the ribbon. Then you have the option to include a Visio 2010 drawing:

      Virtualized Visio 2010 within locally installed Word 2010.

      Virtualized Visio 2010 within locally installed Word 2010.

    • An example:

      An example.

      An example.

    Actions on the client: create the connection group.
    Now only Visio 2010 is made available on the locally installed Word 2010. To include Project 2010, a connection group has to be used.

    • Create the connection group XML File ‘ProjectandVisio.xml’.
      
      
         
           
           
         
      
    • The AppConnectionGroupId and VersionId have been created with the command:
      [Guid]::NewGuid()

      [Guid]::NewGuid()

      [Guid]::NewGuid()

    • The PackageId and VersionId have been found with the command:
      Get-AppVClientPackage -Name *Microsoft*

      Get-AppVClientPackage -Name *Microsoft*

      Get-AppVClientPackage -Name *Microsoft*

      The PackageId and VerionId have been marked.

    • To enable the connectiongroup run the command:
      Add-AppvClientConnectionGroup -Path "C:\AppV50\ProjAndVisio.xml" | Enable-AppVClientConnectionGroup -Global

      Add-AppvClientConnectionGroup -Path "C:\AppV50\ProjAndVisio.xml" | Enable-AppVClientConnectionGroup -Global

      Add-AppvClientConnectionGroup -Path “C:\AppV50\ProjAndVisio.xml” | Enable-AppVClientConnectionGroup -Global

    • Also check the event viewer:

      Check the event viewer.

      Check the event viewer.

    • If you start the locally installed Word 2010, you will see both Project 2010 and Visio 2010:

      Virtualized Project 2010 and Visio 2010 in a locally installed Word 2010.

      Virtualized Project 2010 and Visio 2010 in a locally installed Word 2010.

    • This happens if a Project 2010 document is added:

      Project 2010 document.

      Project 2010 document.

    • If you see this warning please wait a short while and try again:

      Warning.

      Warning.

    Cleaning up.

    First remove the connection group:

    • Run the command:
      Get-AppVClientConnectionGroup | Remove-AppVClientConnectionGroup

      Then all the connection groups are removed.

      Get-AppVClientConnectionGroup | Remove-AppVClientConnectionGroup

      Get-AppVClientConnectionGroup | Remove-AppVClientConnectionGroup

    • And check the event viewer for the results:

      Event Viewer - Remove ConnectionGroup

      Event Viewer – Remove ConnectionGroup

    Remove the virtual Microsoft applications
    remove all the Microsoft applications. As only visio 2010 and Project 2010 have been added as a virtual Microsoft application, a wildcard can be used.

    • Run the command:
      Get-AppVClientPackage -Name *Microsoft* | Remove-AppVClientPackage

      Get-AppVClientPackage -Name *Microsoft* | Remove-AppVClientPackage

      Get-AppVClientPackage -Name *Microsoft* | Remove-AppVClientPackage

    Known errors

    • Error:

      Add-AppvClientConnectionGroup : Application Virtualization Service failed to  complete requested operation.  Operation attempted: Add AppV Connection Group.  AppV Error Code: 0600000008.  Error module: Catalog. Internal error detail: 8A90160600000008.  Please consult AppV Client Event Log for more details.

      Add-AppvClientConnectionGroup : Application Virtualization Service failed to
      complete requested operation.
      Operation attempted: Add AppV Connection Group.
      AppV Error Code: 0600000008.
      Error module: Catalog. Internal error detail: 8A90160600000008.
      Please consult AppV Client Event Log for more details.

      Event Viewer

      Event Viewer

      In this case the connection group is installled, but one of the packages mentioned in the connection group XML File has not been added on the client.

    • In the runvirtual registry key it has no sense to add the AppConnectionGroupId and VersionId of the connection group XML file. It is completely ignored. Just add one of the PackageID / VersionId of one of the virtual applicatoins mentioned in the connection group XML file.

    Example: linked objects in Excel

    As an example both Visio 2010 and Project 2010 will be made available in a locally installed Excel 2010. A Visio 2010 object and a Project 2010 object will be inserted into Excel 2010.

    • Allow the locally installed Excel 2010 to run in a virtual environment:
      • Start regedit and go to HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\AppV\Client\RunVirtual
      • Create the key ‘excel.exe’.
      • Under the (Default) value, add both the PackageID and VersionID of Visio. There is a underscore between both the PackageID and VersionID.

      Allow the locally installed Excel 2010 to run in a virtual environment

      Allow the locally installed Excel 2010 to run in a virtual environment

    • Create a Visio 2010 object in Excel 2010:
      • Start Excel and go to ‘Insert’and select ‘Object’ from the ribbon.
      • Select Visio Drawing from the list.

      Create a Visio 2010 object in Excel 2010.

      Create a Visio 2010 object in Excel 2010.

      • Click [Ok]

      Visio 2010: Change drawing type.

      Visio 2010: Change drawing type.

      • Click [Ok].
    • Drag and drog some figures in Visio.
      Drag and drop some figures in Visio 2010.

      Drag and drop some figures in Visio 2010.

    • When ready click somewhere in the Excel sheet.
      When ready click somehwere in the sheet.

      When ready click somehwere in the sheet.

    • Do the same but then for Project.
      Insert Project 2010 object  in Excel.

      Insert Project 2010 object in Excel.

    • Dubble click on the Visio object. It is opened with Visio.
      Dubble click on the Visio 2010 object.

      Dubble click on the Visio 2010 object.


      Save the Excel sheet and quit Excel.
    • Remove the locally installed Excel 2010 to run in a virtual environment:Use regedit to modify (Default) for Excel: just put an underscore in front of the data.
      Disallow the locally installed Excel 2010 to run in a virtual environment

      Disallow the locally installed Excel 2010 to run in a virtual environment

    • Start Excel and open the saved sheet.
      Click on the visio 2010 object.
      Now there is a failure: “Cannot start the source application for this object.”

      Now there is a failure: “Cannot start the source application for this object.”


      Click [Ok].
      Now there is a failure: “Cannot start the source application for this object.”
    • The same when there is a double click on the Project object.
      Project 2010: Now there is a failure: “Cannot start the source application for this object.”

      Project 2010: Now there is a failure: “Cannot start the source application for this object.”


      Quit Excel without saving the sheet.
    • Allow the locally installed Excel 2010 to run in a virtual environment by removing the underscore.
      Allow the locally installed Excel 2010 to run in a virtual environment

      Allow the locally installed Excel 2010 to run in a virtual environment

    • And the Visio 2010 object works again.
      Visio 2010 works again.

      Visio 2010 works again.

    More information