i'm trying to create a script that checks for permissions of SmbShares on a few Servers.
The goal is to check if any share has a "everyone" permission. However i trying a find a more elegant solution than this mess:
$ADComputer = Get-ADComputer -Filter 'Name -like "Test-Server-*' | Where-Object { $excludeList -notcontains $_.name }
foreach ($server in $ADComputer) {
$shares = Invoke-Command -ComputerName $server -ScriptBlock { Get-SmbShare }
foreach ($share in $shares) {
$permissions = Invoke-Command -ComputerName $server -ScriptBlock { Get-SmbShareAccess -Name $share.name }
foreach ($permission in $permissions) {
if ( $permission ) ...
}
}
}
I can't wrapp my head around this. How can i solve this without using so many nested for loops?
Any ideas or hints?
A good pattern to follow here is:
Solve the whole problem for local execution on a single machine
Use Invoke-Command to repeat across multiple remote hosts
So let's start by writing a single function/script to solve the problem locally first:
function Find-SmbShareEveryoneGrant {
# We can pipe the output from one cmdlet to the next, and solve the problem in a single pipeline
Get-SmbShare |Get-SmbShareAccess |Where-Object { $_.AccessControlType -eq 'Allow' -and $_.AccountName -eq 'Everyone'}
}
Now that you have a function that can be repeated anywhere, it's as simple as calling Invoke-Command against all the servers - the -ComputerName parameter accepts an array, so explicitly looping over $ADComputers is not necessary:
Invoke-Command -ComputerName $ADComputer -ScriptBlock ${function:Find-SmbShareEveryoneGrant} |Select PSComputerName,Name,ScopeName,AccessRight
4-5 lines of code total, much more concise than all those nested loops and successive calls to Invoke-Command
Related
So, I'm trying to remotely remove some manually added printer from a separate machine. Problem is, I don't want to remove all of the printers, only ones that have "HJK" or "LSG" anywhere in the name.
So, to get the list of names, I do:
Invoke-Command AaronsComputer -ScriptBlock {get-printer | select name -expandproperty name}
and to delete the printer, I would do:
Invoke-Command AaronsComputer -ScriptBlock {Remove-Printer -Name "Full Printer Name Here"}
I know I can export the results of get-printer as a CSV to work with it better but I'd rather not do that and have it all happen in Powershell.
I thought something along these lines would work but I don't think PowerShell intelligently sees each line as a variable.
$PrinterResults = Invoke-Command $Computer -ScriptBlock {get-printer | select name -expandproperty name}
foreach $PrinterResults
if ($PrinterResults -contains "HJK") {
Invoke-Command $Computer -ScriptBlock {Remove-Printer -Name "$PrinterResults"}
}
if ($PrinterResults -contains "LSG") {
Invoke-Command $Computer -ScriptBlock {Remove-Printer -Name "$PrinterResults"}
}
The end goal is that I can delete all printers that match the criteria in one go.
Managed to get this done in the end with the below:
Invoke-Command $Computer -ScriptBlock {
$Printers = get-printer | Where-Object {($_.name -like '*HJK*') -or ($_.name -like '*LSG*')} | Select Name -expandproperty name
foreach ($PrinterName in $Printers)
{
printui.exe /dl /n $PrinterName
}
}
(I am a newbie to PS who has mostly used VBS and Batch so I am still working on following PS scripts)
I need to delete most (but not all) domain accounts off of all 500 of our systems.
Some of these are from a specific list.
Some follow a generic format *21, *19, etc...
I can find scripts that will let me delete a specific user account but I can't figure out how to pass it the long list or use the wildcards...
This one seems promising if I can figure out how to get the values need in it...
:: This Script taken from https://www.nextofwindows.com/delete-user-profiles-on-a-remote-computer-in-powershell
$Computer = Read-Host "Please Enter Computer Name: "
$user = Read-Host "Enter User ID: "
Invoke-Command -ComputerName $computer -ScriptBlock {
param($user)
$localpath = 'c:\users\' + $user
Get-WmiObject -Class Win32_UserProfile | Where-Object {$_.LocalPath -eq $localpath} |
Remove-WmiObject
} -ArgumentList $user
It sounds like you are most of the way there. Just use some pipelines and a foreach loop or two.
This will attempt to remove all users in the list from all computers in the list:
# define function that takes user list from pipeline
function Remove-User {
[cmdletBinding()]
param(
[Parameter(ValueFromPipeline=$true,Mandatory=$true)]
[string]
$user,
[Parameter(Mandatory=$true)]
[string]
$computer
)
process {
# copied from question
Invoke-Command -ComputerName $computer -ScriptBlock {
Get-WmiObject -Class Win32_UserProfile |
Where-Object { $_.LocalPath -eq "c:\users\${$user}" } |
Remove-WmiObject
} -ArgumentList $user
}
}
# get your lists whatever way makes sense
$userList = Import-Csv -Path "users.csv" -Delimiter ','
$computerList = Import-Csv -Path "computers.csv" -Delimiter ','
# call function to remove all users for each computer
$computerList | ForEach-Object {
$userList | Remove-User -computer $_
}
I'm not sure where you get your list from, but I used a csv just because.
*note: This is going off the assumption that the Invoke-Command portion of code from nextOfWindows does what it says
I'm struggling to have two conditions met, somehow it seems only one of them works. I'm trying to do :
If a user is connected AND NOT at the lock screen, ask for permission. The commands themselves have been verified and work individually but I must be missing something. Here is what i have:
if (Get-WmiObject –ComputerName $poste –Class Win32_ComputerSystem | Select-Object -expandproperty UserName -and -not (get-process -ComputerName $poste -name logonui)) {
"ask for permission"
}
Right now it just doesn't go in this code, it skips to the lower part where something else is happening.
What is wrong with my syntax ?
I can work around it and make it work this old fashioned way from my CMD days:
Clear-Variable -name statut_user
$statut_user -eq 0
if (Get-WmiObject –ComputerName $poste –Class Win32_ComputerSystem | Select-Object -expandproperty UserName) {$statut_user +=1}
if (-not (get-process -ComputerName $poste -name logonui)) {$statut_user += 1}
if ($statut_user -eq 2) {
"ask for permission"
}
It works, but not as clean as a proper one liner with the two conditions. Thank you for your help!
ANSWER EDIT: Thanks to vonPryz's answer below i ended up using :
$utilisateur = Get-WmiObject –ComputerName $poste –Class Win32_ComputerSystem | Select-Object -expandproperty UserName
$ecran_verr = get-process -ComputerName $poste -name logonui
if( -not ($ecran_verr) -and ($utilisateur)) {
"ask for permission"
}
Aim for clarity, not small codebase size. Instead of making WMI calls in the if statement and piping the results, consider something more readable. Like so,
$cs = gwmi -computername $p Win32_ComputerSystem
$uname = $cs | select-object -expandproperty UserName
$logonui = get-process -ComputerName $p -name logonui
if( -not ($logonui) -and ($uname )) {
# Stuff
}
This approach makes it easy to check that the WMI objects contain sensible values, whatever those may be. Then it should be easier to write a concise conditional statement.
While breaking an expression down into multiple steps is always a good idea for debugging, as demonstrated in vonPryz's helpful answer, sometimes you do want the concision of a single expression without auxiliary variables.
What is wrong with my syntax?
You're missing (...) around the Get-WmiObject ... | Select-Object ... pipeline.
To use a command or pipeline as part of a larger expression, you must always enclose it in (...) A command in PowerShell is a call to an executable - be it a cmdlet, function, alias, or external program.
A simple example:
# !! BROKEN: tokens `-eq 'jdoe'` are interpreted as *arguments for Select-Object*
# !! rather than as operator -eq and RHS 'jdoe'
if (Get-WmiObject Win32_ComputerSystem | Select-Object -Expand UserName -ne 'jdoe') {
'not jdoe'
}
# OK: (...) around the pipeline properly embeds it in the overall expression:
if ((Get-WmiObject Win32_ComputerSystem | Select-Object -Expand UserName) -ne 'jdoe') {
'not jdoe'
}
Here's a fixed version of your original command that fixes that improves other aspects too:
if (
(Get-WmiObject –ComputerName $poste –Class Win32_ComputerSystem).UserName `
-and -not `
(Get-Process -ErrorAction SilentlyContinue -ComputerName $poste -name logonui)
) {
"ask for permission"
}
Given that your Get-WmiObject call only ever outputs 1 object, you can access the .UserName property directly, which is also more efficient than piping to Select-Object -ExpandProperty.
Get-Process outputs a non-terminating error if a process by the given name cannot be found, so -ErrorAction SilentlyContinue suppresses that.
Note the use of ` as a line-continuation character, which allows spreading the conditional across multiple lines, making it much more readable.
Note that the ` must be at the very end of the line.
I am learning to create new objects and combine properties from other objects. In this script I want to find out what the PS version is and also add some other properties like OS, IP etc... but I am running into 2 problems. We have 6 domains and I can't seem to iterate over each domain. I tried (Get-ADForest).Domains and can see the list of domains. It still only returns objects in the domain my workstation belongs to. The second issue is the Invoke-Command. The version always returns 5. I know many of the servers being returned do not have PSVersion 5.
function Get-PSVersion {
(Invoke-Command -Scriptblock {$PSVersionTable.PSVersion}) | Select Major
}
$servers = Get-ADComputer -Filter {(enabled -eq $true) -and (OperatingSystem -like "Windows Server* *")} -Properties * |
ForEach-Object {
$ps = Get-PSVersion
$server = $_
New-Object -TypeName PSObject -Property #{
Name = $server.Name
OS = $server.OperatingSystem
IPAddress = $server.IPv4Address
Location = $server.CanonicalName
PSVersion = $ps.Major
}
}
$servers | Select Name,Location,OS,IPAddress,PSVersion | FT -AutoSize
Ok so starting with the Invoke-Command, You need to tell that cmdlet which server to target, just calling it as you loop over server names will keep calling it on your local computer, so you'll need to use the -computername parameter, and provide your function an argument to pass to invoke-command. Which would look something like this:
function Get-PSVersion($name) {
(Invoke-Command -ComputerName $name -Scriptblock {$PSVersionTable.psversion | Select Major})
}
You'll notice I also moved your select, this isn't strictly necessary but imo it looks cleaner and means slightly less data gets sent over the network. note that this will create an object with a single property called Major, if you want just the version number returned as an integer you'd want to do it like this
function Get-PSVersion($name) {
(Invoke-Command -ComputerName $name -Scriptblock {$PSVersionTable.psversion.Major})
}
You'll need to add an extra loop into the script if you want to target more than one domain, basically you want an array of the domains you wish to target and then loop over that array calling get-adcomputer for each and specifying the domain name for the -server parameter. I've put a simplified example below you can incorporate into your own code.
$arr = #("test.domain","othertest.domain")
foreach($domain in $arr){
Get-ADComputer -Filter * -Server $domain
}
Hope that helps!
Got it to work. Thanks for the assistance.
clear-host
$arr = #("x.local","x.local")
foreach($domain in $arr){
$servers = (Get-ADComputer -Filter {(enabled -eq $true) -and (OperatingSystem -like "Windows Server* *")}-Server $domain -Properties *|Select -First 10)
}
$MasterList = #()
foreach ($server in $servers) {
$MyObj = New-Object PSObject -Property #{
Name = $server.Name
Os = $server.OperatingSystem
Ip = $server.IPv4Address
PSV = Invoke-Command -ComputerName $server.Name -ScriptBlock {$PSVersionTable.psversion}
}
$MasterList += $MyObj
}
$MasterList|Select Name,PSV,OS,IP
Complete powershell and scripting noob here - I don't even know enough to be dangerous. I need to query all PCs in the domain to determine what their local Administrators group membership is and send that output to a text/csv file.
I've tried numerous things like:
Import-Module -Name ActiveDirectory
Get-ADComputer -filter * |
Foreach-Object {
invoke-command {net localgroup administrators} -EA silentlyContinue |
} |
Out-File c:\users\ftcadmin\test.txt
Which gets me an identical list repeated but seems to be hitting every domain PC. I'm guessing it's not actually running on the remote PCs though. Also tried:
$computers = Get-Content -Path c:\users\ftcadmin\computers.txt
invoke-command {net localgroup administrators} -cn $computers -EA silentlyContinue
Get-Process | Out-File c:\users\ftcadmin\test.txt
which is limited by predetermined list of PCs in the computers.txt file. A third thing I tried was this:
$a = Get-Content "C:\users\ftcadmin\computers.txt"
Start-Transcript -path C:\users\ftcadmin\output.txt -append
foreach ($i in $a)
{ $i + "`n" + "===="; net localgroup "Administrators"}
Stop-Transcript
which seems to have the most potential except the output is just
computername1
====
computername2
====
etc without any listing of the group members.
Any ideas from the community?
Copy and paste this function into your PowerShell console.
function Get-LocalGroupMember
{
[CmdletBinding()]
param
(
[Parameter()]
[ValidateNotNullOrEmpty()]
[string]$Name,
[Parameter()]
[ValidateNotNullOrEmpty()]
[string]$ComputerName = 'localhost',
[Parameter()]
[ValidateNotNullOrEmpty()]
[pscredential]$Credential
)
process
{
try
{
$params = #{
'ComputerName' = $ComputerName
}
if ($PSBoundParameters.ContainsKey('Credential'))
{
$params.Credential = $Credential
}
$sb = {
$group = [ADSI]"WinNT://./$using:Name"
#($group.Invoke("Members")) | foreach {
$_.GetType().InvokeMember("Name", 'GetProperty', $null, $_, $null)
}
}
Invoke-Command #params -ScriptBlock $sb
}
catch
{
Write-Error $_.Exception.Message
}
}
}
Then, try this to use it:
Get-ADComputer -filter * | foreach {Get-LocalGroupMember -Name 'Administrators' -ComputerName $_.Name }
If you'd like to do some formatting you could get a list of computers and have all of the members beside each one.
Get-ADComputer -filter * | foreach {
$members = Get-LocalGroupMember -Name 'Administrators' -ComputerName $_.Name
[pscustomobject]#{
'ComputerName' = $_.Name
'Members' = $members
}
}
This requires at least PowerShell v3. If you don't have that, I highly recommend upgrading to PowerShell v4 anyway.
In powershell version 5.1, the version that comes with WMF 5.1 or Windows 10 version 1607, there are now (finally) cmdlets for managing local users and groups. https://technet.microsoft.com/en-us/library/mt651681.aspx
For example, to get the members of the local Administrators group, you could use
Get-LocalGroupMember -Group "Administrators"
Unfortunately, these new cmdlets don't have a ComputerName parameter for running the command remotely. You'll need to use something like Invoke-Command to run the command remotely and the remote computer will need to have powershell version 5.1 as well.
The ComputerName parameter of Invoke-Command cmdlet doesn't accept pipeline input and it only accepts strings, so we first need to expand the name property returned by Get-ADComputer and store the strings in a variable. Thankfully the parameter accepts multiple strings, so we can simply use the $names variable in a single invoke-command call. Example below:
$names = Get-ADComputer -Filter * | Select-Object -ExpandProperty name
Invoke-Command -ScriptBlock {Get-LocalGroupMember -Group "Administrators"} -ComputerName $names
This is actually something that I have recently worked on a fair bit. It seems that there are two conventional ways to get members from the local Administrators group: WMI and ADSI.
In my opinion better method is to use a WMI query to get the members as this includes domain, so you know if the user/group listed is local to the server or is a domain account.
The simpler way using ADSI does not include this information, but is less likely to get Access Denied types of errors.
Towards this end I have cobbled together a script that checks AD for machines that have been active in the last 30 days (if it's not online, there's no need to waste time on it). Then for each of those it tries to do a WMI query to get the admin members, and if that fails it resorts to an ADSI query. The data is stored as a hashtable since that's a simple way to manage the nested arrays in my opinion.
$TMinus30 = [datetime]::Now.AddDays(-30).ToFileTime()
$Domains = 'ChinchillaFarm.COM','TacoTruck.Net'
$ServerHT = #{}
$Servers = ForEach($Domain in $Domains){Get-ADObject -LDAPFilter "(&(objectCategory=computer)(name=*))" -Server $Domain | ?{$_.lastLogonTimestamp -gt $TMinus30}}
$Servers.DNSHostName | %{$ServerHT.Add($_,$Null)}
ForEach($Server in ($Servers | ?{$(Test-Connection $_.DNSHostName -Count 1 -Quiet)})){
Try{
$GMembers = Get-WmiObject -ComputerName $Server -Query "SELECT * FROM win32_GroupUser WHERE GroupComponent = ""Win32_Group.Domain='$Server',Name='Administrators'"""
$Members = $GMembers | ?{$_.PartComponent -match 'Domain="(.+?)",Name="(.+?)"'}|%{[PSCustomObject]#{'Domain'=$Matches[1];'Account'=$Matches[2]}}
}
Catch{
$group = [ADSI]("WinNT://$Server/Administrators,group")
$GMembers = $group.psbase.invoke("Members")
$Members = $GMembers | ForEach-Object {[PSCustomObject]#{'Domain'='';'Account'=$_.GetType().InvokeMember("Name",'GetProperty', $null, $_, $null)}}
}
$ServerHT.$Server = $Members
}
Then you just have to output to a file if desired. Here's what I would do that should output something like what you want:
$ServerHT.keys|%{"`n"+("="*$_.length);$_;("="*$_.length)+"`n";$ServerHT.$_|%{"{0}{1}" -f $(If($_.Domain){$_.Domain+"\"}), $_.Account}}
This would give you something like the following if the first two servers responded to WMI queries and the third did not:
===========
ServerSQL01
===========
ServerSQL01\SQLAdmin
TacoTruck\TMTech
TacoTruck\Stan2112
======
XWeb03
======
ChinchillaFarm\Stan2112
============
BrokenOld486
============
TMTech
Guest
That last one would trigger some red flags in my book if somebody put the Guest account in the local admin group, but I suppose that's probably one of the reason that you're doing this in the first place.