Shortcut List script - powershell

I've created a script that will read a list of usernames in a file named Users.csv and then search a network share for their user profile. It then reports on the shortcut target paths of said user profile and exports that to another .csv.
It works great when I use it per user but when I get it to report the data to the .csv file I can only seem to get it to report on the last user in my Users.csv and not each one. I was thinking it is because the export part of the script overwrites the report.csv for each user it runs and I need it to create a unique report.csv for each username. Anyone have any ideas?
$Users = (Get-Content C:\temp\Users.csv) -notmatch '^\s*$'
foreach ($User in $Users) {
$Shortcuts = Get-ChildItem -Recurse \\UNCPATHTOUSERPROFILES\$User -Include *.lnk
$Shell = New-Object -ComObject WScript.Shell
$data = foreach ($Shortcut in $Shortcuts) {
[PSCustomObject]#{
ShortcutName = $Shortcut.Name;
Target = $Shell.CreateShortcut($Shortcut).targetpath
User = $User
}
}
[Runtime.InteropServices.Marshal]::ReleaseComObject($Shell) | Out-Null
}
foreach ($User in $Users) {
$data | Export-Csv c:\temp\report.csv -NoTypeInformation
}

You should combine your two outer foreach loops, not only since they're iterating the same collection, but because the second loop is trying to use a variable, $data, created in the first loop. Further, since Export-Csv is being called in a loop, you will need to pass the -Append parameter to prevent the output file from being overwritten each time.
$Users = (Get-Content C:\temp\Users.csv) -notmatch '^\s*$'
foreach ($User in $Users) {
$Shortcuts = Get-ChildItem -Recurse \\UNCPATHTOUSERPROFILES\$User -Include *.lnk
$Shell = New-Object -ComObject WScript.Shell
$data = foreach ($Shortcut in $Shortcuts) {
[PSCustomObject]#{
ShortcutName = $Shortcut.Name;
Target = $Shell.CreateShortcut($Shortcut).targetpath
User = $User
}
}
$data | Export-Csv c:\temp\report.csv -Append -NoTypeInformation
[Runtime.InteropServices.Marshal]::ReleaseComObject($Shell) | Out-Null
}
You could also eliminate the $Shortcuts and $data variables in favor of using the pipeline...
$Users = (Get-Content C:\temp\Users.csv) -notmatch '^\s*$'
foreach ($User in $Users) {
$Shell = New-Object -ComObject WScript.Shell
Get-ChildItem -Recurse \\UNCPATHTOUSERPROFILES\$User -Include *.lnk `
| ForEach-Object -Process {
[PSCustomObject]#{
ShortcutName = $_.Name;
Target = $Shell.CreateShortcut($_).targetpath
User = $User
}
} `
| Export-Csv c:\temp\report.csv -Append -NoTypeInformation
[Runtime.InteropServices.Marshal]::ReleaseComObject($Shell) | Out-Null
}
...but note that -Append is still required. Finally, you could rewrite the whole thing using the pipeline...
$Shell = New-Object -ComObject WScript.Shell
try
{
Get-Content C:\temp\Users.csv `
| Where-Object { $_ -notmatch '^\s*$' } -PipelineVariable 'User' `
| ForEach-Object -Process { "\\UNCPATHTOUSERPROFILES\$User" } `
| Get-ChildItem -Recurse -Include *.lnk `
| ForEach-Object -Process {
[PSCustomObject]#{
ShortcutName = $_.Name;
Target = $Shell.CreateShortcut($_).targetpath
User = $User
} `
} `
| Export-Csv c:\temp\report.csv -NoTypeInformation
}
finally
{
[Runtime.InteropServices.Marshal]::ReleaseComObject($Shell) | Out-Null
}
...so that report.csv is only opened for writing once, with no -Append needed. Note that I am creating a single WScript.Shell instance and using try/finally to ensure it gets released.
In the event a user in Users.csv has no directory under \\UNCPATHTOUSERPROFILES, any of the above solutions and the code in the question will throw an error when Get-ChildItem tries to enumerate that directory. You could fix that by checking that the directory exists first or passing -ErrorAction SilentlyContinue to Get-ChildItem, or you could enumerate \\UNCPATHTOUSERPROFILES and filter on those that occur in Users.csv...
$Shell = New-Object -ComObject WScript.Shell
try
{
# Performs faster filtering and eliminates duplicate user rows
$UsersTable = Get-Content C:\temp\Users.csv `
| Where-Object { $_ -notmatch '^\s*$' } `
| Group-Object -AsHashTable
# Get immediate child directories of \\UNCPATHTOUSERPROFILES
Get-ChildItem -Path \\UNCPATHTOUSERPROFILES -Directory -PipelineVariable 'UserDirectory' `
<# Filter for user directories specified in Users.csv #> `
| Where-Object { $UsersTable.ContainsKey($UserDirectory.Name) } `
<# Get *.lnk descendant files of the user directory #> `
| Get-ChildItem -Recurse -Include *.lnk -File `
| ForEach-Object -Process {
[PSCustomObject]#{
ShortcutName = $_.Name;
Target = $Shell.CreateShortcut($_).targetpath
User = $UserDirectory.Name
} `
} `
| Export-Csv c:\temp\report.csv -NoTypeInformation
}
finally
{
[Runtime.InteropServices.Marshal]::ReleaseComObject($Shell) | Out-Null
}

Related

Optimizing VERY Slow PS Script

I have a PS script that is taking CSV files (the largest being about 170MB) and splitting it into multiple smaller CSVs based on the ListGuid value. It is then taking each file and uploading it to a specific path in SharePoint using PnP based on the Web Guid and List Guid. This script it taking forever to run and I am having trouble finding ways to optimize it. Any help would be appreciated. Here is the script:
$PermissionsFile = Get-ChildItem -Path $downloadFilePath -Filter *.csv
foreach ($file in $PermissionsFile) {
$SiteCollectionReport = Import-Csv -Path "$downloadFilePath/$($file.Name)"
$filteredListTestFile = $SiteCollectionReport | Where-Object {$_.Type -eq "List"}
$groupedListFile = $filteredListTestFile | Select-Object Url, ListGuid -Unique
$subWebsConnection = Connect-SharePoint -WebUrl $SiteCollectionReport.Url[0] -CheckForAppCredentials
$subWebs = Get-PnPSubWebs -Recurse -IncludeRootWeb -Connection $subWebsConnection | Select-Object Url, Id
$permissionsSiteConnection = Connect-SharePoint -WebUrl "https://company.sharepoint.com/sites/edmsc/Internal" -CheckForAppCredentials
foreach ($guid in $groupedListFile) {
$webGuid
$listGuid = $guid.ListGuid
$SiteCollectionReport | Where-Object {$_.ListGuid -like $listGuid -and $_.Type -eq "List"} | Export-Csv -Path "Path\Permissions $($listGuid).csv" -NoTypeInformation
$url = $guid.Url
$siteCollectionName = $url.Split("/")[4]
if ($url.Contains(" ")) {
$url = $url.Replace(" ","%20")
}
$split = $url.substring(0, $url.LastIndexOf("/"))
if ($split.Contains("Lists")) {
$split = $split -split "Lists"
}
foreach ($web in $subWebs) {
if ($web.Url -eq $split) {
$webGuid = $web.Id
#Write-Host "Adding permissions reports for $split"
#Write-Host "List Guid $listGuid"
Write-Host "Web Guid $webGuid"
}
}
$fieldValues = #{"ObjectType"="List/ListItem"; "WebGuid"=$webGuid; "ListGuid"=$listGuid}
#$permissionsSiteWeb = Get-PnPWeb -Connection $permissionsSiteConnection
Add-PnPFile -Path "Path\Permissions $($listGuid).csv" -Folder "SiteCollectionPermissions/$siteCollectionName/$webGuid" -Values $fieldValues -Connection $permissionsSiteConnection
}
Write-Host "Deleting Permissions Files..."
Get-ChildItem -Path "Path" -Include *.csv* -File -Recurse | ForEach-Object { $_.Delete()}
}

Powershell get-acl add user first and last name

I have the following code that returns the ntfs permissions for a particular folder path.
$folders = Get-ChildItem -path d:\test" -recurse -force | ?{ $_.psiscontainer }
$output = #()
foreach($folder in $folders)
{
$rights = Get-Acl -path $folder.fullname
foreach($right in $rights.Access)
{
$properties = [ordered]#{'foldername'=$folder.fullname;'username'=$right.identityreference;'permissions'=$right.filesystemrights}
$output += New-Object -TypeName psobject -Property $properties
}
}
$output | export-csv d:\output\folders_temp1.csv -NoTypeInformation -Encoding UTF8
Though it displays the username along with its respective permissions, I would like to display the first and last name associated to that user from the active directory.
Any ideas on how can that be achieved?
Thank you for your help.
Here is how I would approach this, using Group-Object so that you're not querying Active Directory for the same user over and over for each Access Control List.
It's important to note that #() and += is inefficient and that PSCustomObject can be casted which is also, more efficient than using an ordered hashtable and then converting it to a New-Object.
Another efficiency improvement, thanks to Mathias R. Jessen for his helpful feedback, is to implement a hash table ($map) to have a reference of the IdentityReference already queried, by doing so we would only be querying Active Directory only once per unique user.
# $_.PSIsContainer => Can be replaced with -Directory
$folders = Get-ChildItem -Path "D:\test" -Recurse -Force -Directory
$map = #{}
$output = foreach($folder in $folders)
{
$rights = Get-Acl -Path $folder.fullname
$groups = $rights.Access | Group-Object IdentityReference
foreach($group in $groups)
{
if(-not $map.ContainsKey($group.Name))
{
$ref = Split-Path $group.Name -Leaf
$user = Get-ADUser -LDAPFilter "(name=$ref)"
$map[$group.Name] = $user
}
$aduser = $map[$group.Name]
foreach($acl in $group.Group)
{
[pscustomobject]#{
GivenName = $aduser.GivenName
Surname = $aduser.Surname
Foldername = $folder.Fullname
UserName = $acl.IdentityReference
Permissions = $acl.FilesystemRights
}
}
}
}
$output | Export-Csv D:\output\folders_temp1.csv -NoTypeInformation -Encoding UTF8

Powershell Filter for mathing AD Account to Folder

I am trying to match the samaccountname to the current folder patch, and it would worked fine if the folderpatch was "d:\profile\username" but instead of username its states username_S-1-5-21*....
I use the below code, but is it possible to filter everything behind the _S-1.... so it can match the username to the samaccountname ?
I use the below code, any help would be appreciated
[EDIT: added complete script]
Below is the complete script, so the issue is that i have several user folder with _S-1-5-21* behind the username in the folder of our FXLogic profile folder and need to match the samaccountname with the folder ( username - _S-1-5-21* )
I hope this explanation is more clear, and yes its a SID not a GUID always get them mixed up.
param(
[Parameter(Mandatory=$true)]
$FXLogicFolderPath,
$MoveFolderPath,
$SearchBase,
[string[]]$ExcludePath,
[switch]$FolderSize,
[switch]$MoveDisabled,
[switch]$DisplayAll,
[switch]$UseRobocopy,
[switch]$RegExExclude,
[switch]$CheckFXLogicDirectory)
Check if FXLogicFolderPath is found, exit with warning message if path is incorrect
if (!(Test-Path -LiteralPath $FXLogicFolderPath)){
Write-Warning "FXLogicFolderPath not found: $FXLogicFolderPath"
Check if MoveFolderPath is found, exit with warning message if path is incorrect
if ($MoveFolderPath) {
if (!(Test-Path -LiteralPath $MoveFolderPath)){
Write-Warning "MoveFolderPath not found: $MoveFolderPath"
exit
}}
exit
Main loop, for each folder found under FXLogic folder path AD is queried to find a matching samaccountname
$ListOfFolders = Get-ChildItem -LiteralPath "$FXLogicFolderPath" -Force | Where-Object {$_.PSIsContainer}
Exclude folders if the ExcludePath parameter is given
if ($ExcludePath) {
$ExcludePath | ForEach-Object {
$CurrentExcludePath = $_
if ($RegExExclude) {
$ListOfFolders = $ListOfFolders | Where-Object {$_.FullName -notmatch $CurrentExcludePath}
} else {
$ListOfFolders = $ListOfFolders | Where-Object {$_.FullName -ne $CurrentExcludePath}
}
}}
$ListOfFolders | ForEach-Object {
$CurrentPath = Split-Path -Path $_ -Leaf
Construct AD Searcher, add SearchRoot attribute if SearchBase parameter is specified
$ADSearcher = New-Object DirectoryServices.DirectorySearcher -Property #{
Filter = "(samaccountname=$CurrentPath)"
}
if ($SearchBase) {
$ADSearcher.SearchRoot = [adsi]$SearchBase
}
Use the FullName path to look for a FXLogicdirectory attribute and replace the backslash by the \5C LDAP escape character
if ($CheckFXLogicDirectory) {
$ADSearcher.Filter = "(FXLogicdirectory=$($_.FullName -replace '\\','\5C')*)"
}
Execute AD Query and store in $ADResult
$ADResult = $ADSearcher.Findone()
If no matching samaccountname is found this code is executed and displayed
if (!($ADResult)) {
$HashProps = #{
'Error' = 'Account does not exist and has a FXLogic folder'
'FullPath' = $_.FullName
}
if ($FolderSize) {
$HashProps.SizeinBytes = [long](Get-ChildItem -LiteralPath $_.Fullname -Recurse -Force -ErrorAction SilentlyContinue |
Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue | Select-Object -Exp Sum)
$HashProps.SizeinMegaBytes = "{0:n2}" -f ($HashProps.SizeinBytes/1MB)
}
if ($MoveFolderPath) {
$HashProps.DestinationFullPath = Join-Path -Path $MoveFolderPath -ChildPath (Split-Path -Path $_.FullName -Leaf)
if ($UseRobocopy) {
robocopy $($HashProps.FullPath) $($HashProps.DestinationFullPath) /E /MOVE /R:2 /W:1 /XJD /XJF | Out-Null
} else {
Move-Item -LiteralPath $HashProps.FullPath -Destination $HashProps.DestinationFullPath -Force
}
}
Output the object
New-Object -TypeName PSCustomObject -Property $HashProps
If samaccountname is found but the account is disabled this information is displayed
} elseif (([boolean]((-join $ADResult.Properties.useraccountcontrol) -band 2))) {
$HashProps = #{
'Error' = 'Account is disabled and has a FXLogic folder'
'FullPath' = $_.FullName
}
if ($FolderSize) {
$HashProps.SizeinBytes = [long](Get-ChildItem -LiteralPath $_.Fullname -Recurse -Force -ErrorAction SilentlyContinue |
Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue | Select-Object -Exp Sum)
$HashProps.SizeinMegaBytes = "{0:n2}" -f ($HashProps.SizeinBytes/1MB)
}
if ($MoveFolderPath -and $MoveDisabled) {
$HashProps.DestinationFullPath = Join-Path -Path $MoveFolderPath -ChildPath (Split-Path -Path $_.FullName -Leaf)
Move-Item -LiteralPath $HashProps.FullPath -Destination $HashProps.DestinationFullPath -Force
}
Output the object
New-Object -TypeName PSCustomObject -Property $HashProps
Folders that do have active user accounts are displayed if -DisplayAll switch is set
} elseif ($ADResult -and $DisplayAll) {
$HashProps = #{
'Error' = $null
'FullPath' = $_.FullName
}
if ($FolderSize) {
$HashProps.SizeinBytes = [long](Get-ChildItem -LiteralPath $_.Fullname -Recurse -Force -ErrorAction SilentlyContinue |
Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue | Select-Object -Exp Sum)
$HashProps.SizeinMegaBytes = "{0:n2}" -f ($HashProps.SizeinBytes/1MB)
}
Output the object
New-Object -TypeName PSCustomObject -Property $HashProps
}}
# Construct AD Searcher, add SearchRoot attribute if SearchBase parameter is specified
$ADSearcher = New-Object DirectoryServices.DirectorySearcher -Property #{
Filter = "(samaccountname=$CurrentPath)"
}
if ($SearchBase) {
$ADSearcher.SearchRoot = [adsi]$SearchBase
The S-1-5-21... part is not a GUID, it's a SID (the principal's Security Identifier).
You can use the -replace operator to remove that part of the folder name:
$folderName = 'username_S-1-5-21-2855571654-3033049851-1520320983-9328'
$userName = $folderName -replace '_S-1-5.*$'
After which you can construct the desired LDAP query filter:
$ADSearcher = New-Object DirectoryServices.DirectorySearcher -Property #{
Filter = "(samaccountname=$userName)"
}
Not sure where your username is being derived from but you could do something simple with a regex replace on the $CurrentPath
$currentPath = 'username_S-1-5-21-2855571654-3033049851-1520320983-9328'
$currentPath = $currentPath -replace '_.+'
# AD Searcher here
This will replace everything after the underscore but all usernames will have to be in a consistent format or you'll have to take into account all possible formats that could be encountered.

How do I process multiple CSV files in Powershell and give them output names?

So I'm trying to process CSV files, then giving the output new name. I can do it with one file by explicitly specifying the file name. But is there a way / wildcard I can use to make the script to process multiple files at the same time? Let's just say I want to process anything with .csv as an extension. Here's my script that's used to process a specific file
$objs =#();
$output = Import-csv -Path D:\TEP\FilesProcessing\Test\file1.csv | ForEach {
$Object = New-Object PSObject -Property #{
Time = $_.READ_DTTM
Value = $_.{VALUE(KWH)}
Tag = [String]::Concat($_.SUBSTATION,'_',$_.CIRCUITNAME,'_',$_.PHASE,'_',$_.METERID,'_KWH')
}
$objs += $Object;
}
$objs
$objs | Export-CSv -NoTypeInformation D:\TEP\FilesProcessing\Test\file1_out.csv
You can combine Get-ChildItem and Import-Csv.
Here's an example that specifies different input and output directories to avoid name collisions:
$inputPath = "D:\TEP\FilesProcessing\Test"
$outputPath = "D:\TEP\FilesProcessing\Output"
Get-ChildItem (Join-Path $inputPath "*.csv") | ForEach-Object {
$outputFilename = Join-Path $outputPath $_.Name
Import-Csv $_.FullName | ForEach-Object {
New-Object PSObject -Property #{
"Time" = $_.READ_DTTM
"Value" = $_.{VALUE(KWH)}
"Tag" = "{0}_{1}_{2}_{3}_KWH" -f $_.SUBSTATION,$_.CIRCUITNAME,$_.PHASE,$_.METERID
}
} | Export-Csv $outputFilename -NoTypeInformation
}
Note that there's no need for creating an array and repeatedly appending it. Just output the custom objects you want and export afterwards.
Use the Get-Childitem and cut out all the unnecessary intermediate variables so that you code it in a more Powershell type way. Something like this:
Get-CHhilditems 'D:\TEP\FilesProcessing\Test\*.csv' | % {
Import-csv $_.FullName | % {
New-Object PSObject -Property #{
Time = $_.READ_DTTM
Value = $_.{VALUE(KWH)}
Tag = '{0}_{1}_{2}_{3}_KWH' -f $_.SUBSTATION, $_.CIRCUITNAME, $_.PHASE, $_.METERID
}
} | Export-CSv ($_.FullName -replace '\.csv', '_out.csv') -NoTypeInformation
}
The Get-ChildItem is very useful for situations like this.
You can add wildcards directly into the path:
Get-ChildItem -Path D:\TEP\FilesProcessing\Test\*.csv
You can recurse a path and use the provider to filter files:
Get-ChildItem -Path D:\TEP\FilesProcessing\Test\ -recurse -include *.csv
This should get you what you need.
$Props = #{
Time = [datetime]::Parse($_.READ_DTTM)
Value = $_.{VALUE(KWH)}
Tag = $_.SUBSTATION,$_.CIRCUITNAME,$_.PHASE,$_.METERID,'KWH' -join "_"
}
$data = Get-ChildItem -Path D:\TEP\FilesProcessing\Test\*.csv | Foreach-Object {Import-CSV -Path $_.FullName}
$data | Select-Object -Property $Props | Export-CSv -NoTypeInformation D:\TEP\FilesProcessing\Test\file1_out.csv
Also when using Powershell avoid doing these things:
$objs =#();
$objs += $Object;

Import-CSV match multiple keyword

In a nutshell, I have an excel file that I need :
- Only 2 Columns (ComputerName, Results)
- Only need rows that contain specific items (IE. Start with DriveLetter:\, HKLM, %windir%, etc.)
I'm just not sure on the proper keyword syntax here. The original file is an xlsx.
Please forgive the crudeness of my script. I gathered bits and pieces trying to get it to work.
#File Import to Variable
Function Remove-File($fileName) {
if(Test-Path -path $fileName) { Remove-Item -path $fileName }
}
$excelFile = ".\Computers.xlsx"
if(Test-Path -path $excelFile) {
$csvFile = ($env:temp + "\" + ((Get-Item -path $excelFile).name).Replace(((Get-Item -path $excelFile).extension),".csv"))
Remove-File $csvFile
$excelObject = New-Object -ComObject Excel.Application
$excelObject.Visible = $false
$workbookObject = $excelObject.Workbooks.Open($excelFile)
$workbookObject.SaveAs($csvFile,6) # http://msdn.microsoft.com/en-us/library/bb241279.aspx
$workbookObject.Saved = $true
$workbookObject.Close()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($workbookObject) | Out-Null
$excelObject.Quit()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($excelObject) | Out-Null
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
$spreadsheetDataObject = Import-Csv -path $csvFile # Use the $spreadsheetDataObject for your analysis
Remove-File $csvFile
}
#Filter Out All Columns except ComputerName, Results and subseqently create a CSV file
$PathCSV = ".\Computers.csv"
$spreadsheetDataObject | Select-Object ComputerName,Results | Export-Csv -Path $PathCSV -NoTypeInformation
$Keywords = "*HKLM*","*C:\*","*%windir%*"
$Filter = "($(($Keywords|%{[RegEx]::Escape($_)}) -join "|"))"
Import-CSV $PathCSV | Where-Object{$_Results -match $Keywords} | Export-Csv -Path ".\Computers2.csv" -NoTypeInformation
I found the issue. I needed the _.Results..... and I needed to -match $Filter
Import-CSV $PathCSV | Where-Object{$_.Results -match $Filter} | Export-Csv -Path ".\Computers2.csv" -NoTypeInformation