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-cscr...'############################### 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.... ;-)