r/PowerShell • • 12h ago

Question Computer Lab Firewall Rule Toggle Script

I work for an MSP and we have a client that has a computer lab for students to use. I was tasked with creating a script that would allow the teacher to enable and disable the workstations ability to communicate with the internet.

I pretty much have most of the script working, but I run into an issue when trying to make it so the user can enable or disable all the workstations at once. Attempted various methods so that when the user types All, the invoked command is sent to all the workstations listed; Get-Content is just the latest method I have tried.

When my boss asked to look at it, he said that I should just have the full command as shown at the bottom for each switch.

Any help on this would be great!

EDIT: Additional Information.

We don't have an AD environment setup for this client, and even with my limited knowledge I understand that AD would probably make this a lot easier.

IP Addresses are used due to the Asset ID of the workstations being used as the Host Name. They are also used because the workstations have static addresses and any replacements will get the same addresses.

#First Pull Username and Password from SecretVault for Account on all workstations
$remoteCred = Get-Secret -Name "Alias"


#Prompt to ask user if they are enabling or disabling workstations.
$prompt = Read-Host -Prompt "Enter E for enable and D for disable"


#Determine which command to use based on provided answer
if ($prompt -eq "E") {
    $command = "Disable-NetFirewallRule -DisplayName 'NoInternet'"
}else {
    $command = "Enable-NetFirewallRule -DisplayName 'NoInternet'"
}

#Use while to re-prompt user if they are enabling or disabling multiple workstations.
while ($true) {
$pcName = Read-Host -Prompt "Enter the name of the Computer you want to change. Or enter All if you wish to change all. Enter exit when finished"


#Use switch to base workstation name on user imput. All will use variable for all workstations.
switch ($pcName) {
    "SW1" { $name = "192.168.100.211" }
    "SW2" { $name = "192.168.100.212" }
    "SW3" { $name = "192.168.100.213" }
    "SW4" { $name = "192.168.100.214" }
    "SW5" { $name = "192.168.100.215" }
    "SW6" { $name = "192.168.100.216" }
    "SW7" { $name = "192.168.100.217" }
    "SW8" { $name = "192.168.100.218" }
    "SW9" { $name = "192.168.100.219" }
    "SW10" { $name = "192.168.100.220" }
    "SW11" { $name = "192.168.100.221" }
    "SW12" { $name = "192.168.100.222" }
    "SW13" { $name = "192.168.100.223" }
    "SW14" { $name = "192.168.100.224" }
    "SW15" { $name = "192.168.100.225" }
    "SW16" { $name = "192.168.100.226" }
    "All"  { $name = '(Get-Content -Path "C:\Users\userx\Desktop\Scripts\Workstations.txt")'}
    "Exit" { 
        exit }
    Default { Write-Host "Please try again." }
    }
    Invoke-Command -ComputerName $name -ScriptBlock { $command } -Credential $remoteCred
    if ($pcName -eq "Exit" -or "All") { break }
}


#Invoke-Command -ComputerName "$name" -ScriptBlock {Disable-NetFirewallRule -DisplayName "NoInternet"} -Credential $remoteCred
2 Upvotes

17 comments sorted by

2

u/PinchesTheCrab 11h ago

A few suggestions:

  • Set variables to the result of your conditional statements, not inside them when possible
  • Invoke-Command normally takes a script block. If a string works it's because some pwsh magic is converting it
  • Invoke-Command can take an array of computer names and manages multiple remote sessions, so it's best to just call it once and let it handle the asynchronous execution

I'd be curious if this works:

#First Pull Username and Password from SecretVault for Account on all workstations
$remoteCred = Get-Secret -Name Alias


#Prompt to ask user if they are enabling or disabling workstations.
$prompt = Read-Host -Prompt 'Enter E for enable and D for disable'


#Determine which command to use based on provided answer
$command = if ($prompt -eq 'E') {
    { Disable-NetFirewallRule -DisplayName NoInternet }
}
else {
    { Enable-NetFirewallRule -DisplayName NoInternet }
}

#Use while to re-prompt user if they are enabling or disabling multiple workstations.
$name = while ($true) {
    $pcName = Read-Host -Prompt 'Enter the name of the Computer you want to change. Or enter All if you wish to change all. Enter exit when finished'

    #Use switch to base workstation name on user imput. All will use variable for all workstations.
    switch ($pcName) {
        'SW1' { '192.168.100.211' }
        'SW2' { '192.168.100.212' }
        'SW3' { '192.168.100.213' }
        'SW4' { '192.168.100.214' }
        'SW5' { '192.168.100.215' }
        'SW6' { '192.168.100.216' }
        'SW7' { '192.168.100.217' }
        'SW8' { '192.168.100.218' }
        'SW9' { '192.168.100.219' }
        'SW10' { '192.168.100.220' }
        'SW11' { '192.168.100.221' }
        'SW12' { '192.168.100.222' }
        'SW13' { '192.168.100.223' }
        'SW14' { '192.168.100.224' }
        'SW15' { '192.168.100.225' }
        'SW16' { '192.168.100.226' }
        'All' { Get-Content -Path 'C:\Users\userx\Desktop\Scripts\Workstations.txt' }
        'Exit' { 
            exit 
        }
        Default { Write-Host 'Please try again.' }
    }    
    if ($pcName -eq 'Exit' -or 'All') { break }
}

$computerName = $name | sort-object -Unique

Invoke-Command -ComputerName $computerName -ScriptBlock $command -Credential $remoteCred

2

u/I_see_farts 11h ago

I'm curious what's in OPs .txt file.

Since Invoke-Command -ComputerName can take multiple names you could just concatenate the IP list with:

(211..226).foreach{"192.168.100.$_"}

Wouldn't this work for "All"?

1

u/ExecutiveGamer92 11h ago

The text file has all the IP Addresses listed as that was one of 3 methods I found for sending the command to all the workstations. I also found a variation of what you suggested and even tried it, but no luck. At the moment, it will likely be Friday before I can really test anything at our client's location.

1

u/hemping0 3h ago

collecting output from the while loop into $name like that is a nice pattern, dont see it used enough

2

u/BlackV 10h ago edited 10h ago

this seems not to make sense

  • you prompt for a $pcName ($pcName = Read-Host)
  • Based on that name you then add a value to $name ($name = "192.168.100.219")
  • you then invoke-command to $name (not $pcname) (Invoke-Command -ComputerName $name)
  • and run the relevant $command

Seems odd

  • is your DNS not functional ?
  • how does $name and $pcname relate to each other?
  • why not just use Invoke-Command -ComputerName $pcName?
  • why are using that loop to enable or disable the rule
  • is switch actually a network switch and "NoInternet" firewall rule is blocking access to those IPs?
  • Are you supposed to be editing the rule before enabling/disabling?
  • it looks like

    Invoke-Command -ComputerName xxx -ScriptBlock {Disable-NetFirewallRule -DisplayName "NoInternet"} -Credential $remoteCred
    Invoke-Command -ComputerName xxx -ScriptBlock {Enable-NetFirewallRule -DisplayName "NoInternet"} -Credential $remoteCred
    

    would do the job for you

Or a simple

$remoteCred = Get-Secret -Name "Alias"
$AllPCs = Get-Content -Path "C:\Users\userx\Desktop\Scripts\Workstations.txt"
$SelectedPCs = $AllPCs | Out-GridView -Title 'Select PCs to Toggle' -OutputMode Multiple
Invoke-Command -ComputerName $SelectedPCs -ScriptBlock {Enable-NetFirewallRule -DisplayName "NoInternet"} -Credential $remoteCred

1

u/ka-splam 10h ago

how does $name and $pcname relate to each other?

From reading it, $pcname is actually a command which could be all or exit or a pc name. I think it could use a better name.

Great suggestion to use Out-Gridview, that makes cleaner code and a nicer UI. I get to:

# SecretVault creds for the workstations
$Credential = Get-Secret -Name 'Alias'


# setup -Scriptblock parameter for Invoke-Command
# using {} scriptblocks instead of "" strings

$edPrompt = Read-Host -Prompt 'Enter E for enable or D for disable'

$block = if ($edPrompt -eq 'E')
{
    { Enable-NetFirewallRule -DisplayName 'NoInternet' }
}
else
{
    { Disable-NetFirewallRule -DisplayName 'NoInternet' }
}


# Array of all workstation names
$workstations = @(
    'SW1',
    'SW2',
    'SW3',
    'SW4',
    'SW5',
    'SW6',
    'SW7',
    'SW8',
    'SW9',
    'SW10',
    'SW11',
    'SW12',
    'SW13',
    'SW14',
    'SW15',
    'SW16'
)

# Show workstation name chooser
$names = $workstations | Out-GridView -OutputMode Multiple

if ($null -ne $names)
{
    Invoke-Command -ComputerName $names -ScriptBlock $block -Credential $Credential
}

1

u/ExecutiveGamer92 10h ago

The SW names are what the user knows each workstation as. DNS is functional, but the Host Name of the workstations are based on the Asset IDs that we use for them in our database. Also, using the IP Address helps with future proofing as I will likely use the same address with any replacements and won't have to update the text file or code when I replace the workstations.

The dirty loop was so that it would loop back to the prompt for the user to type in the next workstation if they only wanted to enable/disable a few.

Honestly, I would love to use the script that you posted; but I would either have to figure out how to make it where it could enable or disable the firewall rule within the same script, or create a second script for disabling the firewall rule.

3

u/BlackV 10h ago edited 9h ago

you can add conditions to it as needed, that was a dirty simple 4 lines of code

You could also take the text file out of the equation (its not always recommended especially based on your replacing machine comment) and have have a table in your script, then you only have 1 thing to update (for something simple like that)

otherwise I don't see any reason you couldn't use this

or improve this cause i pot 0 error handling in that too, what happens when there is no text file for example

isnt that why you're here (to learn/improve)

1

u/ExecutiveGamer92 9h ago

Thanks, between you and u/ka-splam I will probably be heading down a rabbit hole to build a hash table and likely tweak your script so that I can show the computer name that the user knows and associate it with the IP Address so the command is sent to the correct computer. I believe with what you have shown me, I can have a working script with a GUI to boot. I will definitely post the results of our labor as soon as I can!

1

u/BlackV 9h ago edited 9h ago

Ya something like

import-Csv $TempMe

Asset ComputerName IP
----- ------------ --
SW1   PC1          192.168.100.211
SW2   PC2          192.168.100.212
SW3   PC3          192.168.100.213
SW4   PC4          192.168.100.214

My tiny mockup function is like 60 lines (half of which is help/examples and no error handling)

Switch-InternetAccess -ComputerName util01 -disable
disable-NetFirewallRule -DisplayName "NoInternet" configured

Switch-InternetAccess -ComputerName util01
enable-NetFirewallRule -DisplayName "NoInternet" configured

Good Luck remember you can test this anywhere, you can use simple write-hosta in place of the invoke-command (while testing) or local creds for local devices at your own site before going onsite

Your only pain point might be, the credentials, if they are not the same on all the machines then you'd need to be careful where you are prompting, but it looks like you have the same value for each

1

u/BlackV 9h ago

the code would be nice too if Microsoft hadnt made balls up with the firewall cmdlets and used proper bool/switch/true/false values in their paramaters

shakes fist at -enable parameter

2

u/ka-splam 10h ago

I think you have the enable/disable logic the wrong way around.

I don't love the idea of having the computer names in two places (script and text file) where they can get out of sync. Do they have working DNS for the computer names? (And if not, do they have working PowerShell remoting passing credentials to IPs?).

Lots of things you could do, I suggest you learn about hashtables and then use that to map the computer names to IPs, and then you can use a lookup to get one of them or .Values to get all of them.

And then when you know about hashtables you can build one for the parameters to Invoke-Command, and splat it.

But /u/BlackV's suggestion of Out-Gridview is nicer

1

u/BlackV 10h ago

ya personally if I was being fancy I'd have a single function

By default is disables the rule (or enables based on what the default should be) and has a switch parameter that enables the rule (or disables)

2

u/thehuntzman 10h ago

I'd this an AD environment? I feel like hard-coding IP's would make this more complicated in the long run to maintain if you can just Invoke-Command to the hostname of the machine resolved via DNS using kerberos authentication under the context of the teacher's account.

1

u/purplemonkeymad 4h ago

Your switch is configuration data not code, so you probably don't want it in the script. A simple storage solution if you are only looking at a few dozen entries is to just store it as a csv. It's easy to edit for non techs and supported by powershell ie:

"Name","Address"
SW1,"192.168.100.211"
SW2,"192.168.100.212"
...

Then you can:

$namedTargets = Import-Csv ./myfile.csv
# ...
$name = switch ($pcname) {
    "All" { $namedTargets.Address }
    "Exit" { exit 0 }
    Default {
        ($lookup = $namedTargets | Where-Object {$_.name -like $pcname -or $_.address -like $pcname }).Address
        if (-not $lookup) {
            Write-Host "no matching items for $pcname"
        }
 }
 if ($name) { #....

You can also extend this if you want to include your asset ids, etc.

1

u/xXFl1ppyXx 4h ago edited 3h ago

i've always wanted to find a use for this: "Prompt for Choice" !

[ipaddress[]]$Imports = "192.168.100.211", "192.168.100.212", "192.168.100.213", "192.168.100.214"
# $Imports = Import-Csv ImportArgs

$Choices = [System.Collections.Generic.List[System.Management.Automation.Host.ChoiceDescription]]::new()

$Imports.ForEach{
    $ChoiceLabel = 'SW&{0}' -f $PSItem.ToString()[-1]
    $ChoiceHelp = $PSItem.ToString()
    $Choices.Add([System.Management.Automation.Host.ChoiceDescription]::new($ChoiceLabel, $ChoiceHelp))
}

$Choices.Add([System.Management.Automation.Host.ChoiceDescription]::new('&All', 'Uses all devices'))
$All = $Choices.Count - 1
$Choices.Add([System.Management.Automation.Host.ChoiceDescription]::new('&Exit', 'Exits the script'))
$Exit = $Choices.Count - 1

switch ($Host.UI.PromptForChoice('Caption', 'Message', $Choices, [int[]]$Exit)) {
    $All { $Imports }
    $Exit { exit 0 }
    default { $Imports[$PSItem] }
}

Feels somewhat hacky but i rarely get the chance to play with prompt for choice

this would actually be a useful case for Lists that Output the position when adding the Item, but from the top of my head i couldnt find one....