When documenting our server installations, one of the time-consuming documentation line-items is the Windows Firewall. For our service development flexibility and speed, our Windows firewall management is not centralised. This has the disadvantage that firewall auditing is a local operation.
Historically, documenting the firewall configuration meant going into the Windows firewall control panel applet and ennumerating the rules by hand. Later, we used the power of netsh to display the firewall config which has worked well, but still involved a lot of manual editing of the output.
Today's I'll demonstrate the script we now use to give a unified output for our firewall rules across 2003/2008 and 2012 servers. It's a vbscript that parses netsh output to provide firewall rules in a standardised, delimited format. It's not a powershell script as the required firewall cmdlets are only present in Windows 2012 Server and above, rendering a cross-platform solution impossible.
The script provides,
- Single line, delimited rules.
Netsh output has the scope definitions listed as an entry on a new line which means it's fiddly to import the output into a table.
- Aliasing
The scope fields care often unreadable in environments where multiple IP and subnet are present. This script allows you to create aliases for common subnets and IPs to make your rules more readable
- Standardised Output
Netsh output can change with OS. The script understands that Windows 2003 server output differs slightly from 2008/2012 and hides this.
It also provided a bit of fun, as working with regular expressions is always a joy.
The Firewall_Audit.VBS Script
The script detailed below should be pasted into a notepad and saved (or use the zipped download attached to this article). When the script is executed, it will create a txt file in the same location as the script called Firewall_Audit-%COMPUTERNAME%.txt. This text file will contain the system's enumerated Windows firewall rules.
'#####################################################################################
'#
'# Script: FirewallAudit.vbs
'#
'# Description: Script to provide simplified windows firewall configuration across
'# 2003,2008,2012 servers.
'#
'# Author: Ian Atkin
'#
'# Version: 1.0
'#
'#
'#####################################################################################
'_____________________________________________________
' Declarations
'¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
Dim myAliases()
Class IPtoName
Public IP_or_Subnet
Public Name
End Class
'_____________________________________________________
' Set the column delimeter
'¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
'sDelim=vbTab
sDelim=","
'_____________________________________________________
' Configure IP and subnet shortform replacements for easier rule viewing
' For example, to change the subnet 10.0.0.0/255.255.255.0 in the netsh
' output to [PRIVATE_1] set the alias as follows,
'
' SetAlias "10.0.0.0/255.255.255.0","[PRIVATE_1]"
'¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
SetAlias "*","ALL"
'_____________________________________________________
' Ensure we run this using cscript.exe
'¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
forceCScriptExecution
'_____________________________________________________
' Set name of output file
'¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
Set objShell = WScript.CreateObject("WScript.Shell")
sCompname=objShell.ExpandEnvironmentStrings( "%COMPUTERNAME%" )
set ofso=CreateObject("Scripting.FileSystemObject")
StrCurDir=ofso.GetParentFolderName(Wscript.ScriptFullName)
sOutFile=StrCurDir & "\Firewall_Audit_" & sCompname & ".txt"
snetshFile=StrCurDir & "\Firewall_Audit_" & sCompname & ".log"
'make sure we don't have this file existing already
if ofso.FileExists(sOutFile) then ofso.DeleteFile(sOutFile)
call AppendTxtToFile("Created by Firewall_Audit.vbs "& NOW(),sOutfile)
call AppendTxtToFile("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~",sOutfile)
call AppendTxtToFile("",sOutfile)
'_____________________________________________________
' Prepare Regular Expression
'¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
Set myRegExp = New RegExp
myRegExp.IgnoreCase = True
myRegExp.Global = false
myRegExp.Multiline = true
'_____________________________________________________
' GetOSVersion
'¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
OSVersionExpression="^\s*([0-9]+).([0-9]+).([0-9]+)$"
myRegExp.Pattern = OSVersionExpression
Set objWMIService = GetObject("winmgmts:{impersonationLevel=impersonate}!\\.\root\cimv2")
Set oss = objWMIService.ExecQuery ("Select * from Win32_OperatingSystem")
For Each os in oss
sVersion=os.version
If myRegExp.Test(os.version) Then
Set myMatches = myRegExp.Execute(os.version)
set myMatch =myMatches(0)
myosmajorversion=mymatch.submatches(0)
end if
Next
'_____________________________________________________
' Set NetSh regular expressions according to OS
'¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
'In order to get firewall rules, here we define our regular expressions for the
'service, program and port firewall exceptions.
ServiceExpression = "^\s*Enable\s+(\S+)\s+(\S.+\S)\s*Scope:\s+(\S+)\s*$"
ProgramExpression="^\s*Enable\s+(\S+..?bound)\s+(\S.*\S)\s+/\s+(\S.*\S)\s*Scope:\s+(\S+)\s*$"
ProgramExpression_NT5="^\s*Enable\s+(\S+.*)\s+/\s+(\S.*\S)\s*Scope:\s+(\S+)\s*$"
PortExpression="^\s*([0-9]+)\s+(\S+)\s+Enable\s+(\S+)\s+(\S.*\S)\s*Scope:\s+(\S.*\S)\s*$"
PortExpression_NT5="^\s*([0-9]+)\s+(\S+)\s+Enable\s+(\S+.*\S)\s*Scope:\s+(\S.*\S)\s*$"
'_____________________________________________________
' Get Raw Firewall config using netsh
'¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
StrNetshOutput=""
Set objExecObject = objShell.Exec("cmd /c netsh firewall show config verbose=ENABLE")
Do While Not objExecObject.StdOut.AtEndOfStream
StrNetshOutput = StrNetshOutput & vbcrlf & objExecObject.StdOut.ReadLine()
Loop
'call writefile(snetshfile,StrNetshOutput)
'_____________________________________________________
' Parse Raw Config and extract rules
'¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
For ProfileLoop=1 to 2
if ProfileLoop=1 then ProfileStr="domain"
if ProfileLoop=2 then ProfileStr="standard"
'Step through each line of netsh output looking for service configuration information
i=instr(1,lcase(StrNetshOutput),"service configuration for " & ProfileStr & " profile")
j=instr(1, lcase(StrNetshOutput),"allowed programs configuration for " & ProfileStr & " profile")
k=instr(1,lcase(StrNetshOutput),"port configuration for " & ProfileStr & " profile")
l=instr(1,lcase(StrNetshOutput),"icmp configuration for " & ProfileStr & " profile")
'First I want to see if there are any enabled service configuration rules. If present,
'these will appear between i and j
m=i
count=0
Do while m<j
'grab two lines...
mystr=GrabLine(m,StrNetshOutput) & GrabLine(instr(m,lcase(StrNetshOutput),vbcrlf) + 2,StrNetshOutput)
myRegExp.Pattern = ServiceExpression
If myRegExp.Test(mystr) Then
if count=0 then
wscript.echo vbcrlf & "Service Exceptions for " & ProfileStr & " profile" & vbcrlf & "---------------------------------------"
call AppendTxtToFile(vbcrlf & "Service Exceptions for " & ProfileStr & " profile" & vbcrlf & "---------------------------------------",sOutfile)
end if
count=count+1
Set myMatches = myRegExp.Execute(mystr)
set myMatch =myMatches(0)
myservice=mymatch.submatches(1)
myscope=replacealiases(mymatch.submatches(2))
wscript.echo "Service:" & myservice & sDelim & "Scope:" & myscope
call AppendTxtToFile(myservice & sDelim & myscope,sOutfile)
'as we have a match move two lines on
m=instr(m,lcase(StrNetshOutput),vbcrlf) + 2
m=instr(m,lcase(StrNetshOutput),vbcrlf) + 2
else
'No match. Move one line on.
m=instr(m,lcase(StrNetshOutput),vbcrlf) + 2
end if
Loop
'Now search for enabled program exceptions
m=j
count=0
Do while m<k
mystr=GrabLine(m,StrNetshOutput) & GrabLine(instr(m,lcase(StrNetshOutput),vbcrlf) + 2,StrNetshOutput)
if myosmajorversion="5" then
myRegExp.Pattern = ProgramExpression_NT5
else
myRegExp.Pattern = ProgramExpression
end if
If myRegExp.Test(mystr) Then
if count=0 then
wscript.echo vbcrlf & "Program Exceptions for " & ProfileStr & " profile" & vbcrlf & "---------------------------------------"
call AppendTxtToFile(vbcrlf & "Program Exceptions for " & ProfileStr & " profile" & vbcrlf & "---------------------------------------",sOutfile)
end if
count=count+1
Set myMatches = myRegExp.Execute(mystr)
set myMatch =myMatches(0)
if myosmajorversion="5" then
mydir="In"
myprogname=mymatch.submatches(1)
myprogpath=mymatch.submatches(2)
myscope=replacealiases(mymatch.submatches(2))
else
mydir=replace(mymatch.submatches(0),"bound","")
myprogname=mymatch.submatches(1)
myprogpath=mymatch.submatches(2)
myscope=replacealiases(mymatch.submatches(3))
end if
wscript.echo mydir & sDelim & "Name:" & myprogname & sDelim & "Path:" & myprogpath & sDelim & "Scope:" & myscope
call AppendTxtToFile(mydir & sDelim & myprogname & sDelim & myprogpath & sDelim & myscope,sOutfile)
'as we have a match move two lines on
m=instr(m,lcase(StrNetshOutput),vbcrlf) + 2
m=instr(m,lcase(StrNetshOutput),vbcrlf) + 2
else
'No match. Move one line on.
m=instr(m,lcase(StrNetshOutput),vbcrlf) + 2
end if
Loop
'Now search for port exceptions
m=k
count=0
Do while m<l
'grab two lines...
mystr=GrabLine(m,StrNetshOutput) & GrabLine(instr(m,lcase(StrNetshOutput),vbcrlf) + 2,StrNetshOutput)
'now test to see if these match our regular expression
if myosmajorversion="5" then
myRegExp.Pattern = PortExpression_NT5
else
myRegExp.Pattern = PortExpression
end if
If myRegExp.Test(mystr) Then
'We have a match!
if count=0 then
wscript.echo vbcrlf & "Port Exceptions for " & ProfileStr & " profile" & vbcrlf & "------------------------------------"
call AppendTxtToFile(vbcrlf & "Port Exceptions for " & ProfileStr & " profile" & vbcrlf & "------------------------------------",sOutfile)
end if
count=count+1
Set myMatches = myRegExp.Execute(mystr)
set myMatch =myMatches(0)
if myosmajorversion="5" then
myport=mymatch.submatches(0)
myprotocol=mymatch.submatches(1)
mydir="In"
myname=mymatch.submatches(2)
myscope=replacealiases(mymatch.submatches(3))
else
myport=mymatch.submatches(0)
myprotocol=mymatch.submatches(1)
mydir=replace(mymatch.submatches(2),"bound","")
myname=mymatch.submatches(3)
myscope=replacealiases(mymatch.submatches(4))
end if
'as we have a match move two lines on
m=instr(m,lcase(StrNetshOutput),vbcrlf) + 2
m=instr(m,lcase(StrNetshOutput),vbcrlf) + 2
wscript.echo "Port:" & myport & sDelim & myprotocol & sDelim & mydir & sDelim & myname & sDelim & "Scope:" & myscope
call AppendTxtToFile(myprotocol & ":" & myport & sDelim & mydir & sDelim & myname & sDelim & myscope,sOutfile)
else
'No match. Move one line on.
m=instr(m,lcase(StrNetshOutput),vbcrlf) + 2
end if
Loop
wscript.echo
wscript.echo
call AppendTxtToFile("",sOutfile)
call AppendTxtToFile("",sOutfile)
next
'#############################################################
'#
'# FUNCTIONS AND SUBS
'#
'#############################################################
'###############################
'# Get a whole line from a multi-line string,
'# starting from a specific character position
'###############################
Function GrabLine(index,InputStr)
'This function returns as a string all characters leading up to the line break
'from the string index provided
'wscript.echo "Hello-IN"
myIndex=Instr(index,InputStr,vbcrlf)
'wscript.echo "myIndex:" & myIndex
'wscript.echo "Index:" & index
GrabLine=Mid(InputStr,index,myIndex-index)
'wscript.echo myIndex
'wscript.echo "*" & GrabLine & "*"
'wscript.echo "Hello-OUT"
End Function
'###############################
'# Write line to txt file
'###############################
Sub AppendTxtToFile(sTxt,sFilename)
Dim sFile, fso, ts
Set fso = CreateObject("Scripting.FileSystemObject")
Set ts = fso.OpenTextFile(sFilename, 8, True)
ts.WriteLine sTxt
ts.close
Set ts = Nothing
Set fso = Nothing
End Sub
'###############################
'# Read text file
'###############################
function GetFile(FileName)
If FileName<>"" Then
Dim FS, FileStream
Set FS = CreateObject("Scripting.FileSystemObject")
on error resume next
Set FileStream = FS.OpenTextFile(FileName)
GetFile = FileStream.ReadAll
if Err.Number<>0 then
'msgbox "Can't read File"
wscript.quit ERR_FILEREAD
End If
End If
End Function
'###############################
'# Write string As a text file.
'###############################
function WriteFile(FileName, Contents)
Dim OutStream, FS
on error resume Next
Set FS = CreateObject("Scripting.FileSystemObject")
Set OutStream = FS.OpenTextFile(FileName, 2, True)
OutStream.Write Contents
if Err.Number<>0 then
'msgbox "Can't write File"
wscript.quit ERR_FILEWRITE
End If
End Function
'###############################
'# Alias routines
'###############################
Sub SetAlias(StrIP,StrName)
on error resume next
i=ubound(myAliases)
on error goto 0
redim preserve myAliases(i+1)
set myAliases(i+1)=New IPtoName
myAliases(i+1).IP_or_Subnet=StrIP
myAliases(i+1).Name=StrName
End Sub
Function ReplaceAliases(myString)
for i=1 to ubound(myAliases)
myString=replace(myString,myAliases(i).IP_or_Subnet,myAliases(i).Name)
next
ReplaceAliases=myString
End Function
'###############################
'# Force run using cscript
'# From http://stackoverflow.com/questions/4692542/force-a-vbs-to-run-using-cscript-instead-of-wscript
'###############################
Sub forceCScriptExecution
Dim Arg, Str
If Not LCase( Right( WScript.FullName, 12 ) ) = "\cscript.exe" Then
For Each Arg In WScript.Arguments
If InStr( Arg, " " ) Then Arg = """" & Arg & """"
Str = Str & " " & Arg
Next
CreateObject( "WScript.Shell" ).Run _
"cscript //nologo """ & _
WScript.ScriptFullName & _
""" " & Str
WScript.Quit
End If
End Sub
How the Script works
This script is simply a wrapper for netsh. It launches the command,
netsh firewall show config verbose=ENABLE
and grabs the output courtesy of the StdOut.AtEndOfStream property. This output is then parsed to figure out where each section in the netsh output begins.The sections we are interested in are those that detail the service, program and port firewall execeptions (for both the standard and domain profiles).
Once we have identified where each of these sections lies in the output string, we can then use regular expression matching (tuned to each section) to extract the rule properies we need so that we can output them in a more standardised way. We omit at this stage the ICMP and logfile config sections, and for the scope entries perform a search and replace so humanise the output.
Adding IP and Subnet Aliases to the Script
We keep this script on a central file server, and within it we keep updated our favorite server IPs and site subnets. These IPs and subnets are automatically replaced in the scope output of netsh to provide a more human-readable output.
To add these, just locate the SetAlias call in vbscript which has the following single entry
SetAlias "*","ALL"
Now expand this to cater for your environment. So, for example you might end up with something like,
SetAlias "10.0.0.0/255.255.255.0","[PRIVATE_1]"
SetAlias "10.0.1.0/255.255.255.0","[PRIVATE_2]"
SetAlias "10.0.0.1/255.255.255.255","SITE-SERVER1"
SetAlias "10.0.1.1/255.255.255.255","SITE-SERVER2"
SetAlias "10.0.0.2/255.255.255.255","TEST-SMP76"
When you next run the script, any scopes which match the above definititions will automagically be replaced.
Further Work
I have been tempted to split out the aliasing piece in this script so that the aliases could be defined separately in another comma delimeted pair text file. That way, the script would not need to be edited directly to taylor it to new environments.
One idea was to automatically create this alias file by exporting our server IP and subnet details from our Altiris SMP directly. It would be neat, but likely a bit more trouble than it's worth.... ;-)