Powershell improperly reading data in a CSV - powershell

I currently am writing a Powershell script that remotely removes users from a local admin group on a list of servers. The CSV headers are Computer and Name. For each entry of user (name), matches the server (computer).
Ex.
Computer,Name
Server1,User1
Server1,User2
Server2,User1
Script:
$List = Import-CSV C:\temp\LocalAdmin.CSV
$user = $List.Name
$objGroup = $List.Computer
write-host "Removing user" $user "from server" $objGroup "local admin group:" -ForegroundColor Green
Invoke-Command -ComputerName $objGroup -ScriptBlock {Remove-LocalGroupMember -Group "Administrators" -Member $using:user }
write-host "Completed."
When the script runs, it runs through perfectly fine the first time through, but then it runs through the script line by line for how many ever lines there are causing it to attempt to remove the users multiple times. Can someone help me fix this logic? It is almost like the CSV is being read as an array vs a list. I appreciate the help!

I would say:
$List = Import-CSV C:\temp\LocalAdmin.CSV
ForEach ($Item in $list)
{
$user = $Item.Name
$objGroup = $Item.Computer
write-host "Removing user $user from server $objGroup local admin group:" -ForegroundColor Green
Invoke-Command -ComputerName $objGroup -ScriptBlock {Remove-LocalGroupMember -Group "Administrators" -Member $user }
}
write-host "Completed."
Not tested, but this should be your starting point.

As for what you tried:
Given that $List contains an array of objects, with each element containing an object representing a row of the CSV file, $List.Name and $List.Computer similarly return arrays of property (column) values, courtesy of PowerShell's member-access enumeration
Therefore, $using:user refers to the array of all usernames, across all servers.
While the -Member parameter of Remove-LocalGroupMember does accept arrays, there are two problems with your approach:
At least hypothetically you'll run the risk of deleting users you shouldn't from certain servers, or you'll run into users that don't exist on a given server (though you could ignore that with -ErrorAction Ignore).
Since a given server name can appear multiple times in the CSV, the targeted user(s) will have already been deleted, starting with the second call to that server - this is the problem you saw.
TheStingPilot's helpful answer
provides an effective solution: loop over the objects representing the CSV rows one by one, and call Invoke-Command for each target server, with only the username at hand.
The downside of this approach - which may or may not matter, depending on how many computers you target - is that forgo the benefits of parallel execution that you get when you pass multiple computer names to Invoke-Command's -Computer parameter (by default, up to 32 computers at a time are targeted in parallel; you can modify that number with -ThrottleLimit).
To avoid multiple calls to a given server while preserving the benefits of parallel execution:
Build a hashtable from the CSV input that maps server names to user names.
Pass that hashtable to a single Invoke-Command call, as you tried, and let each remote computer look up the relevant usernames in it and act on them.
# Read the CSV file and create a hashtable (map) that
# maps server (computer) names to usernames to remove.
$serverUserMap = [ordered] #{}
Import-CSV C:\temp\LocalAdmin.CSV |
ForEach-Object {
[array] $serverUserMap[$_.Computer] += $_.Name
}
# * $serverUserMap.Keys now contains all unique server names,
# which can be passed to -ComputerName
# * Inside the script block, accessing the hashtable with the local
# computer name as the key returns only the relevant user(s).
Invoke-Command -ComputerName $serverUserMap.Keys -ScriptBlock {
Remove-LocalGroupMember -Group "Administrators" -Member ($using:serverUserMap)[$env:COMPUTERNAME]
}

Related

Pulling Win10 activation codes from remote computers

I'm brand new to PS scripting, so bear with me :)
I'm trying to create a PS script that will write the Win10 activation code to a file then copy that file to a central repo to then manually activate.
I'm creating a PS script and trying to run
cscript.exe c:\windows\system32\slmgr.vbs -dti >
$SourceDir\$env:computername.txt
$SourceDir = \\computer01\c$\temp
I need to run it from one computer, remotely connecting to every computer on the network, creating the computername.txt file then copying that file back to a central repository for all the files.
What I have so far:
$s1=New-PSSession -ComputerName computer01 -Credential $AdminCred
Test-Connection -ComputerName computer01
$id='\\computer01\windows\system32'
$SourceDir='\\computer01\c$\temp'
md $SourceDir
$GetActID=cscript.exe $id\slmgr.vbs -dti >
$SourceDir\$env:computername.txt
Invoke-Command -Session $s1 -ScriptBlock { $Using:GetActID }
Then I call a batch file that copies the computername.txt file from the computer01 over to a repository where they are going to sit.
I FINALLY got it working correctly except for the name of the file isn't naming it to the computer01, it's naming it with the hostname of the computer I'm running it from, therefore the filenames are identical. I had the naming piece working, but I had to change the way I was remoting into the computer and now it's not naming correctly.
Any idea on how I could get it to name the file to be related to the remote computer?
**I'm still working on the whole piece of the puzzle where it goes back to an excel sheet pulled from AD and pulls the host names from that sheet to connect to each machine, I believe I'll be adding a ForEach syntax in there somehow for that.
Although not sure how you are getting the list of "every computer on the network", chances are you are doing this using
# get a list of all AD computers (their names only)
$computers = (Get-ADComputer -Filter *).Name
Then I think you don't need to have every computer save the file on its own disk and later copy these files to a central share.
Instead, just capture the info in a variable and after the loop write the file to the central share as structured CSV file combining all computernames and install id's so you can open in Excel.
Using the array of computernames from above, iterate through them
$result = $computers | ForEach-Object {
# test if the computer can be reached
if (Test-Connection -ComputerName $_ -Count 1 -Quiet) {
$installId = Invoke-Command -ComputerName $_ -ScriptBlock {
cscript.exe //nologo "$env:SystemRoot\System32\slmgr.vbs" -dti
}
# $installId is returned as array !
# output an object with two properties
[PsCustomObject]#{
Computer = $_
InstallId = $installId[0] -replace '\D' # remove everything non-numeric
}
}
else {
Write-Warning "Computer $_ is not responding"
}
}
# now you can display the result on screen
$result | Format-Table -AutoSize
# or by means of the GridView if you prefer
$result | Out-GridView -Title 'Computer InstallIds'
# and save the results in your central share as structured CSV file
$result | Export-Csv -Path '\\server\share\restofpath\ComputerInstallIds.csv' -NoTypeInformation
You may have to append -Credential $adminCreds to the Invoke-Command call to make sure you have permissions to have each machine run that piece of code in the scriptblock. The easiest way of obtaining that credential is to start off with $adminCreds = Get-Credential -Message "Please enter administrator credentials"

Powershell Get-ciminstance error in foreach Loop

New to Powershell, trying to use it to update our AD inventory with Serial Numbers from all computers in the environment. I have exported a .csv with all the machine names, and am trying to run through that in a loop from my machine
The script runs fine without the loop-- if I run it and swap the $name variable with a string it works, and if I run it and define the variable and don't loop it work.When in the loop I get this error:
WS-Management could not connect to the specified destination: (#{Name="PC1"}:5985).
This is a different error than if I type a hostname that doesn't exist or one that isn't in DNS. It's getting the proper info from the .csv like "PC1". tried using start-sleep -second 2 to give it some time to connect to each machine but I don't think that's helping.
$admachines = import-csv .\Computerinventory.csv
foreach ($hostname in $admachines){
$sn = Get-CimInstance -Class win32_bios -ComputerName $hostname |select-object -expandproperty serialnumber
Set-ADComputer -identity $hostname -replace #{serialNumber = $sn}
}

Powershell GPO Login Script checking AD resource group membership

The system I have to work with uses AD resource group membership to manage most of the permissions for users and computers. I have been asked to improve the current logon script as it currently contains some VB ADSISEARCHER calls. I started trying to do this purely in powershell but have hit a number of hurdles.
Target machines do not have the Active Directory Module installed
The users logging into the system have a restricted user accounts
The resource groups are nested so the script needs to handle this
I have tried a couple of approaches firstly the pure Powershell Cmdlet method of Get-ADGroup or Get-ADPricipalGroupMembership but these Cmdlet's require the Active Directory Module. Then I tried the .net approach with System.DirectoryServices.DirectoryEntry although this is a step away from a pure PowerShell solution at least it isn't as Legacy as the VB route. However when I try to build the object it also appears to be missing the name space.
First Attempt:
function Get-UserResourceMembership
{
[CmdletBinding()]
Param
(
# Username or Groupname to Discover Group Membership
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true,
Position=0)]
$User
)
Begin
{
$Groups = #(Get-ADPrincipalGroupMembership $User)
}
Process
{
foreach($ADGroup in $Groups)
{
if($ADGroup.ObjectClass -eq "Group")
{
Get-UserResourceMembership $ADGroup
}
$GrpMembership = #($ADGroup)
}
}
End
{
return ,$GrpMembership
}
}
Second Attempt:
# $rootGroup is passed in from earlier in the script
$groupname = $rootGroup.'Group Name'
$filter = ("(&(objectCategory=Group)(name=$($groupname)))")
$searcher.Filter = $filter
$searcher.SearchScope = "Subtree"
$searchResults = $searcher.FindAll().GetDirectoryEntry().memberOf |
% { (New-Object System.DirectoryServices.DirectoryEntry("LDAP://"+$_)) } |
Sort-Object Children | select #{name="Group Name";expression={$_.Name}}
foreach($resource in $searchResults)
{
if($resource.'Group Name' -match "<Groupname>")
{
$printResource += $resource
}
}
Does anyone in the community have any suggestions how to pull group membership [nested] from Active Directory from a standard users login script??? Any idea's much appreciated....
PS I can't change the way the system is designed (above my pay grade).
As for ...
• Target machines do not have the Active Directory Module installed
• The users logging into the system have a restricted user accounts
• The resource groups are nested so the script needs to handle this
Does not matter, they do not need to be installed on a client to use
them. You can use PSRemoting to proxy those using 'Implicit
Remoting'. The cmdlets are only available in the remote session.
Does not matter, as every user has read access, by default to ADDS
in Windows.
You can get to those using the cmdlets you are using and
there are even pre-built scripts in the Microsoft PowershellGallery.com
for this as well.
As for …
I have tried a couple of approaches firstly the pure Powershell Cmdlet
method of Get-ADGroup or Get-ADPricipalGroupMembership but these
Cmdlet's require the Active Directory Module.
As noted above, this can be addressed as described below:
PowerShell Implicit Remoting: Never Install a Module Again
Remote Session
# create a session then import a module via the session, for example:
$adsess = New-PSSession -ComputerName savdaldc01
Import-Module -Name ActiveDirectory -PSSession $adsess
Get-Module
Get-ADUser -Filter *
Remove-Module ActiveDirectory
# It's also possible to prefix modules loaded from remote servers to differentiate from local modules, e.g.
Import-Module -Name ActiveDirectory -PSSession $adsess -Prefix OnDC
Get-OnDCADUser -Filter * #I don't have regular Get-ADUser anymore
Remove-Module ActiveDirectory
Remove-PSSession $adsess
As for ...
Does anyone in the community have any suggestions how to pull group
membership [nested]
Get nested group membership - function
This function will recursively enumerate members of a given group
along with nesting level and parent group information. If there is a
circular membership, it will be displayed in Comment column.It accepts
input from pipeline and works well with get-adgroup. Download:
Get-ADNestedGroupMembers.ps1
As well as just doing this...
We can get group members by using the Active Directory powershell
cmlet Get-ADGroupMember. The Get-ADGroupMember cmdlet provides the
option to get all the nested group members by passing the parameter
-Recursive. This powershell script also handles circular membership (infinite loop) problem.
Function Get-ADNestedGroupMembers
{
[cmdletbinding()]
param
(
[String] $Group
)
Import-Module ActiveDirectory
($Members = Get-ADGroupMember -Identity $Group -Recursive)
}
Get-ADNestedGroupMembers "Domain Admins" | Select Name,DistinguishedName
or this way.
function Get-NestedGroupMember
{
param
(
[Parameter(Mandatory, ValueFromPipeline)]
[string]$Identity
)
process
{
Import-Module ActiveDirectory
$user = Get-ADUser -Identity $Identity
$userdn = $user.DistinguishedName
$strFilter = "(member:1.2.840.113556.1.4.1941:=$userdn)"
Get-ADGroup -LDAPFilter $strFilter -ResultPageSize 1000
}
}
All of the methods below list all groups including nested groups.
The example below would execute a gpresult command in the user's context. The gpresult outputs to an XML file within the user's local profile, which they should have full access to already. Then the XML file is read and traversed through each node until you reach the node containing the groups. The group list contains local and domain groups and is outputted directly to the console. This can easily be stored in a variable or output to a file. If you only want domain groups, that could easily be filtered from here with a Regex. It requires that the client machines are running at least Windows Vista SP1 or later.
gpresult /USER "$env:userdomain\$env:username" /X "$env:userprofile\rsop.xml"
$xml = [xml](Get-Content "$env:userprofile\rsop.xml")
$xml.Rsop.UserResults.SecurityGroup.Name."#text" # Displays the groups
Remove-Item "$env:userprofile\rsop.xml" # Removes the XML file
You could also use a potentially use Regex matching to find the group list:
$out = gpresult /R /USER $env:username
$GroupsUnfiltered = (($out | out-string) -split "-{10,}")[-1]
$Groups = ($GroupsUnfiltered.trim() -replace "(?m)^\s+","") -split "(?m)\r?\n"
$Groups
The following can also work if your group list always begins with a predictable group like Domain Users in this example:
$out = gpresult /R /USER $env:username
$GroupList = $out.where({$_ -match "domain users"},'SkipUntil').trim()
$GroupList
This code assumes that the users and machines are joined to the same domain or are at least joined to trusted domains. The second code snippet assumes every user is in the Domain Users group and the machines are natively PowerShell v4 or higher.

Powershell Deployed via SCCM Issue

I am writing a powershell script to be deployed by SCCM via a package. The aim of this is to remove an account with a specific name then write to a file stating if the account exists or not. The code is below:
$Computer = hostname
foreach ($C in $Computer) {
if (Test-Connection $C -Quiet) {
Write-Verbose "$C > Online"
$Users = Get-WMIObject Win32_UserAccount -Filter "LocalAccount=True" -ComputerName $C
if ($Users.Name -contains 'test') {
Add-Content \\SERVERNAME\SHARENAME.$\$computer-found_$(get-date -Format yyyymmdd_hhmmtt).txt "User 'test' found, Disable 'test' found"
net user test /active:no }
else {
Add-Content \\SERVERNAME\SHARENAME.$\$computer-notfound_$(get-date -Format yyyymmdd_hhmmtt).txt "User 'test' not found"
}
}
else {
Write-Verbose "$C > Offline"
}
}
I have also tried replace Write-Verbose with Write-Host and Add-Content with Out-File but the problem I having is that no content / file is created when I use the full network path or share e.g. \\SERVERNAME\SHARENAME.$ the path identified has all the correct permissions and is being ran locally using the System account.
I wanted to see if the issue occured when writing the file locatlly consequently this does not happen when written to C:\Temp\
Does anyone have any ideas on to solve this.
I don't think that local system account has access to a network resource. I'm not sure if you have ever configured it or not. And what the command you used to run the command
Here I post a working way of doing this using Configuration Manager deployment after testing in my lab.
Basically I created a package with source files
and created a task sequence with single "Run Command Line" step.
The reason I use a task sequence is because I want to use an account to access the txt file on the network, which I can configure within a task sequence. I don't think Local System Account have such permission.
The script (DeactivateTest.ps1) I use as below just like what you provided and changed a little on the logic:
$Computer = hostname
foreach ($C in $Computer) {
if (Test-Connection $C -Quiet) {
Write-host "$C > Online"
$Users = Get-WMIObject Win32_UserAccount -Filter "LocalAccount=True" -ComputerName $C
$result=0
Foreach($user in $Users){
if ($User.Name -like '*test*') {
$username = $user.Name
"`n$(get-date -Format yyyymmdd_hhmmtt) User $username found ON $C, Disable 'test'" | Add-Content \\cas\resource\Result.txt
net user $username /active:no
$result+=1
}}
if($result =0){
"`n$(get-date -Format yyyymmdd_hhmmtt) User 'test' not found ON $C" | Add-Content \\cas\resource\Result.txt}
}
else {
"`n$C is Offline" | Add-Content \\cas\resource\Result.txt
}
}
The script query local account and disable accounts which have words "Test" in the name. If you don't like this logic, you can change :).
\\cas\resource\Result.txt is a txt file on the network share. Clients will write result to this txt file.
The command in the task sequence is (it's a x64 machine):
PowerShell.exe -ExecutionPolicy Bypass -File ".\DeactiveTest.ps1"
The output is like:
I may get downvoted for this as my answer isn't technically directly answering your question, it is, however, intended to try and point you in what may be a more logical direction. All apologies if I offend anyone, but here it is:
Why not just disable the user using Group Policy? If you really want to know where the user is/isn't disabled then you could just use hardware inventory for that, but GP really is the best way to enforce this kind of setting.

How can I hardcode specific server names in powershell script

I want to reboot specific servers, I am using findstr to find specific servers in the list of 1000's of servers, however, is there any way I can hardcode servernames in a script so the script only run on a particular set of remote servers? Also, how to use for each against array variable. For e.g is below method correct?
$serverlist = "server1,server2,server3"
for each ($server in $serverlist){ $serverboot= gwmi win32_operatingsystem -comp $server
$serverboot.Reboot
}
First define a list of your servers for this the coma (,) is the array operator in PowerShell :
$serverlist = "server1","server2","server3"
Now $serverlist is an array or a collection you can use.
Then you pipe this list in the Foreach-Object Cmdlet that allow you to execute a scriptblock for each element in the list. $_ represent the current element. ; is the instruction seprator :
$serverlist | ForEach-Object {$serverboot= gwmi win32_operatingsystem -comp $_; $serverboot.Reboot()}