I have written up a script that gives me the ability to delete multiple user profiles and the registry keys associated off of a computer. this is for work, multiple people share computers at a few clinics of ours and eventually the computer gets filled up of user profiles of people that no longer work there or just got their own computer. so the storage ends up running out, thats where i come in, deleting a lot of the user profiles to clean up the disk. i wanted to make this a lot easier so i tried writing a script. here it is. btw this is all domain join. this can be done by group policy i'm aware but my engineers haven't made that happen, i'm just help desk trying to make my life easier.
$profiles = Get-CimInstance -Class Win32_UserProfile
$users = 'schaudhary'
foreach ($profile in $profiles){
if ($profile.Special -ne 'True'){
if ($profile.LocalPath.split('\')[-1] -notcontains $users) {
$profile | Where-Object { $_.LocalPath.split('\')[-1] } | Remove-CimInstance -WhatIf
Write-Host "deleting" $profile.LocalPath
}
}
}
problem is when i try to exclude multiple users from the deletion process, it doesn't work. but when i have just one user, like right now "schaudhary" it'll work (it'll exclude schaudhary). how can i make it exclude multiple users?? I have to exclude the local admin acct, the active users on the machine and some special service accounts. and if anyone can give tips on adding last use time included in here that'll help. so only delete if user is 90 days old or more, something like that.
an event associated with a user would mean that he was working on a computer.
But you have to make sure there are logs for 90 days
$sid =(Get-adUser $username).sid.value
$StartDate = (Get-Date) - (New-TimeSpan -Day 90)
Get-WinEvent -FilterHashtable #{LogName='Security';data=$sid;StartTime=$StartDate} -MaxEvents 1
You are reversing the variables when using -notcontains..
Its use is $collectionOfThings -contains $singleThing or
$collectionOfThings -notcontains $singleThing.
Since PowerShell version 3, you can also do the reverse using -in and -notin as in
$singleThing -in $collectionOfThings or the negation of that
$singleThing -notin $collectionOfThings.
See Containment operators
Try
$profiles = Get-CimInstance -Class Win32_UserProfile
$users = #('schaudhary') # array of profiles to keep
foreach ($profile in $profiles){
if (!$profile.Special) {
$userName = $profile.LocalPath.split('\')[-1]
# or:
# if ($userName -notin $users) {
if ($users -notcontains $userName) {
if (!$profile.Loaded) {
$profile | Remove-CimInstance -Confirm:$false -WhatIf
Write-Host "Deleting $($profile.LocalPath)"
}
else {
Write-Warning "Profile $($profile.LocalPath) is currently loaded.."
}
}
}
}
Related
This is the original script I wrote. PowerShell will not display any of the system profiles in the pre determined array, but it completely disregards the fact that any of the users exist within the csv file. When I run the script, it spits out users that exist within the csv file, but not any of the profiles that match the profiles I have listed in the array.
$ADUsers = Import-CSV -Path C:\users\username\Desktop\DiskSpaceCleanup.csv | Sort SamAccountName
$ExcludedProfiles = #('administrator','Public','default','DOMAIN\administrator','NetworkService','LocalService','systemprofile')
$OnHardDrive = Get-CimInstance -ClassName Win32_UserProfile
$userstoremove = #()
foreach($user in $OnHardDrive){
if(($ADUsers -notcontains $user.LocalPath.split('\')[-1]) -and ($ExcludedProfiles -notcontains $user.LocalPath.split('\')[-1])){
$userstoremove += $user
}
}
$userstoremove.LocalPath | Format-Table
I was expecting this code to output anyone who is not in the $ExcludedProfiles array and anyone that is not in the $ADUsers array.
What actually ends up happening is the users in the csv file and the user profiles are not excluded from the results and the users I specified in the $ExcludedProfiles array are.
Any advice on how to filter out the users in the csv file from the results?
This might be a more robust way of doing it by translating the NTAccounts to their corresponding SID and then comparing against the SID property of the objects returned from Get-CimInstance -ClassName Win32_UserProfile. I only changed SystemProfile to System from your current list to actually target NT AUTHORITY\System.
Worth noting, this solution only works on English locale. Thanks to iRon for pointing that out.
[Collections.Generic.HashSet[Security.Principal.SecurityIdentifier]] $excludedSIDs = #(
(Import-CSV -Path C:\users\username\Desktop\DiskSpaceCleanup.csv).SamAccountName
'Administrator'
'Public'
'Default'
'DOMAIN\Administrator'
'NetworkService'
'LocalService'
'System'
) | ForEach-Object {
try {
[Security.Principal.NTAccount]::new($_).
Translate([Security.Principal.SecurityIdentifier])
}
catch { }
}
$OnHardDrive = Get-CimInstance -ClassName Win32_UserProfile | Where-Object {
$excludedSIDs.Add($_.SID)
}
$OnHardDrive.LocalPath
The following should work on any Windows Locale but includes and uses the WellKnownSidsType Enum to generate the list of SIDs to exclude. The only problem is, this includes more SIDs than what OP might be interested in excluding.
$domainSid = [System.Security.Principal.NTAccount]::new($env:USERNAME).
Translate([System.Security.Principal.SecurityIdentifier]).
AccountDomainSid
[Collections.Generic.HashSet[Security.Principal.SecurityIdentifier]] $excludedSIDs = #(
(Import-CSV -Path C:\users\username\Desktop\DiskSpaceCleanup.csv).SamAccountName |
ForEach-Object {
try {
[Security.Principal.NTAccount]::new($_).
Translate([Security.Principal.SecurityIdentifier])
}
catch { }
}
[Enum]::GetValues([System.Security.Principal.WellKnownSidType]) |
ForEach-Object {
try {
[System.Security.Principal.SecurityIdentifier]::new($_, $domainSid)
}
catch { }
}
)
$OnHardDrive = Get-CimInstance -ClassName Win32_UserProfile | Where-Object {
$excludedSIDs.Add($_.SID)
}
$OnHardDrive.LocalPath
Occasionally I forget to log off from a server or am disconnected through an error and I don't remember the name of the server. And my domain account starts getting periodically locked out, so I have to access logs on DC to find out which server(s) keep locking my account and log off from it/them. So I wanted to write to script in powershell that would log me off from all servers in a domain (with the exception of the server where I run the script on of course) without me needing to search which to log off from. This is what I have:
$ErrorActionPreference = "Silentlycontinue"
$Servers = (Get-ADComputer -Filter *).Name
$ScriptBlock = {
$Sessions = quser | ?{$_ -match $env:USERNAME}
if (($Sessions).Count -ge 1)
{
$SessionIDs = ($Sessions -split ' +')[2]
Write-Host "Found $(($SessionIDs).Count) user login(s) on $Server."
$SessionIDs | ForEach-Object
{
Write-Host "Logging off session [$($_)]..."
logoff $_
}
}
}
foreach ($Server in $Servers)
{
if ($Server -isnot $env:COMPUTERNAME)
{
Invoke-Command -ComputerName $Server -ScriptBlock {$ScriptBlock}
}
}
But when I launch the script, nothing happens. The script doesn't return any errors but doesn't log me off from any server, nor does it write any of the messages from Write-Host cmdlet, obviously. I noticed the $SessionIDs variable definition only returns ID of the first session. Usually this shouldn't be a problem, since it's unlikely I will have more than one session on a server, but I'd like to have this insurance. Can anyone tell me what's wrong in the script?
I notice a few things...
"First, I don't think quser | Where-Object {$_ -match $env:USERNAME} will ever return anything. The output of quser will not contain the hostname."
Try this for getting logon sessions:
$Sessions = (query user /server:$Env:ComputerName) -split "\n" -replace '\s\s+', ';' |
ConvertFrom-Csv -Delimiter ';'
Next, when you reference the $Server variable on the remote machine in your script block, it is out of scope. You would need to use $Using:Server in the script block.
Lastly, the -isnot operator doesn't compare value, it compares type. So in your last foreach, the if statement evaluates to "if type string is not type string" and will not run. Try -ne or -notlike instead.
Working with objects is much easier if you can just parse the output of QUser.exe. Given your scenario, here's my take on it:
$servers = (Get-ADComputer -Filter '*').Name.Where{$_ -ne $env:COMPUTERNAME}
foreach ($server in $servers)
{
if (-not ($quser = ((QUser.exe /server:$server) -replace '\s{20,39}',',,' -replace '\s{2,}',',' 2>&1) | Where-Object -FilterScript { $_ -match $env:USERNAME })) {
Continue
}
Write-Verbose -Message "$($quser.Count) session(s) found on $server." -Verbose
($quser.Trim() | ConvertFrom-Csv -Header 'USERNAME','SESSIONNAME','ID','STATE','IDLE TIME','LOGON TIME').foreach{
Write-Verbose -Message "Logging user [$($_.UserName)] off." -Verbose
LogOff.exe $_.ID /server:$server
}
}
Filtering should always happen before hand meaning, filter out your computer name on your first call to Get-ADComputer. Since you're using QUser.exe and LogOff.exe to begin with, I'd recommend the use of it all the way through since LogOff accepts an ID value that QUser outputs.
Next, placing the call to quser inside your if statement does two things in this case.
Filters for all users matching $ENV:UserName
Returns $true if anything is found, and $false if not found.
So, switching the results using -not will turn $false into $true allowing the execution of the code block which will just continue to the next server.
This in turn doesn't bother with the rest of the code and continues onto the next computer if no matching names were found.
The use of $quser inside the if statement is so you can save the results to it if more than one name is found; (..) allows this as it turns the variable assignment into an expression having the output pass through onto the pipeline where it is either empty, or not.
Finally, referencing the $quser variable we can convert the strings into objects piping to ConvertFrom-Csv. Only step left to do is iterate through each row and passing it over to LogOff to perform the actual logoff.
If you've noticed, the headers are manually-specified because it is filtered out by the Where-Object cmdlet. This is a better approach seeing as there could be "more than one" RDP Session, now you're just left with those sessions matching the name which can be saved to $quser, so no extra filtering is needed down the line.
So I modified the script this way and it works, sort of. It logs off account from servers, which is the main goal. There are still some glitches, like the message it sends from the first Write-Host doesn't give server's name, the message from second one gives a different value than it should (it gives [1] value after -split instead of [2] for some reason; but those are not really that important things, even though I will try to make at least the first message right) and $SessionIDs still gives only the first value, but usually you shouldn't have more than one RDP session per server. I've seen more sessions of one user, but that is very rare. But I'd also like to fix this if possible. Nevertheless, the script basically does the most important thing. But if someone has a suggestion how to fix the glitches I mentioned I would be grateful.
$ErrorActionPreference = "Silentlycontinue"
$Servers = (Get-ADComputer -Filter *).Name
$ScriptBlock = {
$Sessions = quser | ?{$_ -match $env:USERNAME}
if (($Sessions).Count -ge 1)
{
$SessionIDs = , ($Sessions -split ' +')[2]
Write-Host "Found $(($SessionIDs).Count) user login(s) on $Server."
Foreach ($SessionID in $SessionIDs)
{
Write-Host "Logging off session $SessionID..."
logoff $SessionID
}
}
}
foreach ($Server in $Servers)
{
if ($Server -ne $env:COMPUTERNAME)
{
Invoke-Command -ComputerName $Server -ScriptBlock $ScriptBlock
}
}
Iam trying to create a task that will executed later with tasksch on the local machine, that will run everyday.
The task is, if a user has not logged in to the computer, it shall be removed from the computer.
The Thing is there is something wrong with my code and i dont know what
For now iam trying that the script shall remove testuser 1,2,3 and on testuser 4 is shall say Account is not old by using lastusetime.
System-info
OS Name: Microsoft Windows 10 Enterprise
OS Version: 10.0.17763 N/A Build 17763
Account-Info
#{Sid=S-1-5-21-3824012622-276487612-2647460976-1105; LocalPath=C:\Users\testuser4; LastUseTime=2019-05-13 19:27:57}
#{Sid=S-1-5-21-3824012622-276487612-2647460976-1109; LocalPath=C:\Users\testuser3; LastUseTime=2019-05-10 14:54:07}
#{Sid=S-1-5-21-3824012622-276487612-2647460976-1108; LocalPath=C:\Users\testuser2; LastUseTime=2019-05-10 14:54:07}
#{Sid=S-1-5-21-3824012622-276487612-2647460976-1107; LocalPath=C:\Users\testuser1; LastUseTime=2019-05-10 13:49:16}
# Start PS-Code
$DeleteIfLastUseTimetIsOlderThen = (get-date).AddDays(-5).tostring("yyyy-MM-dd hh:mm:ss”)
$GetLocalAccounts = Get-WmiObject -ComputerName localhost -Filter "Special=False" -Class Win32_UserProfile |
Select-Object Sid, LocalPath, #{Label='LastUseTime';Expression={$_.ConvertToDateTime($_.LastUseTime)} }
foreach ($UserAccount in $GetLocalAccounts)
{
if ($GetLocalAccounts.LastUseTime -Ge $DeleteIfLastUseTimetIsOlderThen )
{ Write-host "Account is old, Remove me"}
Else
{ Write-host "Account is not old"}
}
# End PS-Code
The thing is that it doesn't matter if I change the value "$DeleteIfLastUseTimetIsOlderThen " to 1,2,3,4 or 55, everything seems to be old.
The issue here is twofold. One is that you want to use a less than comparison when looking for an older date since a date in the past is less than a date in the future. Second, you want to compare each date in your collection ($GetLocalAccounts) with a specific fixed date ($DeleteIfLastUseTimeIsOlderThan). To do this with your code structure, you will need to check against the current object ($UserAccount) in your for loop.
# Start PS-Code
$DeleteIfLastUseTimetIsOlderThan = (get-date).AddDays(-5).tostring("yyyy-MM-dd hh:mm:ss”)
$GetLocalAccounts = Get-WmiObject -ComputerName localhost -Filter "Special=False" -Class Win32_UserProfile |
Select-Object Sid, LocalPath, #{Label='LastUseTime';Expression={$_.ConvertToDateTime($_.LastUseTime)} }
foreach ($UserAccount in $GetLocalAccounts)
{
if ($UserAccount.LastUseTime -le $DeleteIfLastUseTimetIsOlderThan )
{ Write-host "Account is old, Remove me"}
Else
{ Write-host "Account is not old"}
}
# End PS-Code
Using "net view" I can view all the pcs on the network and browse their hidden shares \PC_NAME\c$ . But I can't find an easy way to search every pc on the network at once.
Is there a way using cmd/powershell to search for .pst .ost files on every machine in the network?
I want to use powershell or command prompt to automate it every few weeks.
You can use something like this:
$credentials = Get-Credential UsernameWithAccess
$servers = #("server1","server2")
foreach ($sever in $servers){
$shares = Get-WmiObject Win32_share -computer netdb -Credential $credentials
foreach ($path in $shares.name){
$content = Get-ChildItem -Recurse -path \\netdb\$path
$List = $content | where {$_.extension -eq ".dll"}
$List | format-table name
}
}
Make sure you change UsernamewithAccess with your user and servernames in second line, and extention from .dll to what you need to search for.
Before I start, please note I am a beginner at Powershell, so some questions I ask may seem very obvious and stupid to the more experienced.
I have a problem with my script. If I copy paste it into Powershell itself, it works with no problems. However putting it in a .ps1 file, and making it execute with Powershell doesn't work. Can anyone tell me why, and what I can do to make it work using a .ps1? Here's the code:
$Group = import-csv -path C:\Output\Gruppe.csv
$DomainUsers = import-csv -path C:\Output\DomainUsers.csv
$ErrorActionPreference = "SilentlyContinue"
Get-ADGroupMember –identity Test –recursive | select "samaccountname" | Export-csv –path C:\Output\Gruppe.csv -NoTypeInformation
Get-ADUser –Filter * -SearchBase ”ou=Domain Users,dc=sfol,dc=local” | select "samaccountname" | Export-csv –path C:\Output\DomainUsers.csv –NoTypeInformation
Compare-Object $Group $DomainUsers -property samaccountname -IncludeEqual | where-object {$_.SideIndicator -eq "=="} | select "samaccountname" | Export-csv C:\Output\Difference.csv –NoTypeInformation
(Get-Content C:\Output\Difference.csv) | % {$_ -replace '"', ""} | out-file -FilePath C:\Output\Difference.csv -Force -Encoding ascii
$File = "C:\Output\Difference.csv"
$Time = Get-Date
ForEach ($User in (Get-Content $File))
{ Try {
Remove-ADGroupMember -Identity "Test" -Member $User -Confirm:$false -ErrorAction Stop
Add-Content c:\Output\Gruppelog.log -Value "$Time - $User slettet fra gruppen"
}
Catch {
Add-Content c:\Output\Gruppelog.log -Value "$Time - $User medlem kunne ikke blive slettet fra gruppen pga: $($Error[0])"
}
}
I also have another problem I noticed as I am writing this question. What this script does is to print out a userlist from an OU and a group. Then it compares the OU to the group using the two files it printed out, and prints out a new userlist containing only the users that exists in both the OU and the group. Then it uses the new userlist to remove users from the group (so that there is no users that exist in both the OU and group).
This script works well the first time I run it, but if I proceed with re-adding the users to the group, running the script again, sometimes it will only remove some of the users. If I do ctrl+c and CLS after running the script, it works fine. As mentioned, I am a beginner at this, so I'd just like to know why it doesn't work 100% the second time without ctrl+c or cls. Sorry if I am bad at explaining, and I don't expect you to help me with this since it's not a part of the question. But I'd appreciate it if you could.
Kind regards, Shadow
Problem was not having imported the module ActiveDirectory. I thought that when you imported the module once, it would stay for future use in Powershell. This isn't so, so what I did to fix the problem was adding Import-Module ActiveDirectory to the beginning of the code. I still haven't figured out my bonus question, so if anyone can help with that, it'd be great.