Disclaimer: This information is provided as-is for the benefit of the Community. Please contact Sophos Professional Services if you require assistance with your specific environment.
Table Of Contents
- Introduction
- What do you need?
- What can you do with this?
- In IPHost "mode":
- In FQDN "mode":
- In Automode (TM) BETA AF
- In Splitter mode
- How can I use it?
- Run It
- Whats next?
Introduction
I'm not a programmer, and I'm not even close; everything here is self-taught and might contain errors. Test it in your environment before using it in real life. Always make backups, TEST the backups, and export full configurations.
I think laziness its the mother of inventive. I prefer 1 hour to create automated things to view more cats memes study more than doing manual stuff. That's why I did a little script to help load mostly IPHosts and FQDN. Helped me a lot when migrating firewalls from other brands since objects it's the more boring stuff to migrate.
What do you need?
- One computer in which you can run a Powershell script
- A list of things to upload to your firewall
- A firewall (duh!). Ideally Sophos, but it works for Palo Alto (!!)
- API Enabled for the computer
What can you do with this?
In IPHost "mode":
You can feed a CSV file with the following format:
Object name,IP addres,netmask
Examples
Gateway IP,192.168.0.254,/27
Printer,172.31.0.15,255.255.255.255
Office AP,172.16.0.10
Web Server,10.10.17.45,24
And he'll feed the firewall and create the appropriate objects. It can read the mask in the following formats:
- /XX (Slash and a number, /24, /16, /30, etc)
- XX (just two numbers, 24, 16, 30, etc)
- xxx.xxx.xxx.xx (normal representation like 255.255.255.0)
- No mask at all (it will assume that its a host ip with /32 mask)
In FQDN "mode":
You can feed a CSV file with the following format:
Object name, FQDN
You can choose to omit the FQDN, and in that case, it will use the name as the FQDN.
Examples
Google,*.google.com
*.amazon.com
www.ebay.com,*.ebay.com
In Automode (TM) BETA AF
You can feed a CSV file with a mix of IPHost/FQDN, and the script will interpret (99,9% of the time) the appropriate object.
Examples
*.amazon.com
85.1.56.7
Gateway IP,192.168.0.254,/27Printer,172.31.0.15,255.255.255.255
Office AP,172.16.0.10
*.microsoft.com
NASA Website,90.43.1.65domain.localhost.com
172.16.14.250
NASA Site FQDN,*.nasa.gov
Assumptions the script does in this mode:
- If it has 1 field (ex: *.amazon.com or 172.16.100.100), it will check if it matches an IP address format (xxx.xxx.xxx.xxx)
- If it does, its considered an IP address with mask /32
- If it doesn't its considered an FQDN (working to improve detection for more cases)
- If it has 2 or more fields, it will check if the second field matches an IP address format (xxx.xxx.xxx.xxx), if it does, its considers an IPHost and same rules for IPHost applies here (if it doesn't have netmask, its assumed an IPHost /32). If the second field doesnt match that, its considered a FQDN host.
In Splitter mode
This method is very simple: It takes existing objects in Sophos XML format and imports them one by one. This is better than the "all at once way" with import, since it will give you instant feedback about what works (and what doesn't) without waiting for the full import.
Specify the XML tag (objects) that you want to import (IPHost, FQDNHost, Services, MACAddress, whatever) and just let the script upload them for you.
Example of XML data:
<IPHost transactionid="">
<Name>172.22.122.142/32</Name>
<IPFamily>IPv4</IPFamily>
<HostType>IP</HostType>
<IPAddress>172.22.122.142</IPAddress>
</IPHost>
<IPHost transactionid="">
<Name>172.22.18.145/32</Name>
<IPFamily>IPv4</IPFamily>
<HostType>IP</HostType>
<IPAddress>172.22.18.145</IPAddress>
</IPHost>
<IPHost transactionid="">
<Name>192.168.10.6/32</Name>
<IPFamily>IPv4</IPFamily>
<HostType>IP</HostType>
<IPAddress>192.168.10.6</IPAddress>
</IPHost>
How can I use it?
# --------------------- # ImportMan - XG # --------------------- # Versions # # 1.0 - Version original # 1.1 - Added code to allow different masks in CSV # 1.2 - Translated to English from Spanish # 1.3 - Multi version - works with fqdn too # 1.4 - Improved iphost detection, fqdn detection. Added automode TM # 1.5 - Improved FQDN detection and cases available. Validation of credentials prior to creationg of objects. Minor translations. # 1.6 - Little bug in reporting folder location. Added some context help on the script code. # 1.7 - Updated with better single object detection. Added prefixes if you want to # 1.8 - Added XML splitter, in case you already have an xml that needs to be imported one by one to Sophos XG. # --------------------- param ($_OPERATION) # Global variable declaration # ------------------------------ # You are required to adjust the following variables for correct working of the script # $_FIREWALL_IP = IP address of the firewall you want to upload API request # $_FIREWALL_PORT = Port on which the HTTPS interface of the firewall is working (normally 4444) # $_API_USER = User that will be used for API calls (normally "admin") # $_API_PASSWORD = Password for the account defined in the previous step # $_WORK_FOLDER = Folder on which the script will work and leave logs/data. Needs to be writable for the user running the script. Needs to end with \ (ex: C:\API\) # $_DATA_FILE_NAME = Name of the file which has the data to be uploaded to the firewall # $_ADD_PREFIXES_TO_OBJECTS = Add a prefix to objects to diferentiate between them. IPs get an "HOST_" prefix, networks gets a "NET_" and FQDN an "FQDN_". You can select "Yes" or "No" # ------------------------------ # Variables you need to adjust for your environment # ------------------------------------------------- $_FIREWALL_IP = "192.168.1.210" $_FIREWALL_PORT = "4444" $_API_USER = "admin" $_API_PASSWORD = 'correcthorsebatterystaple' $_WORK_FOLDER = "D:\OneDrive\Scripts\XG\PowerShell\ImportMan\1.8\" $_DATA_FILE_NAME = "ips.txt" $_ADD_PREFIXES_TO_OBJECTS = "Yes" # Variables that SHOULDN'T be modified by the user # ------------------------------------------------ $_LOGS_FOLDER = ([String](Get-Date -Format 'ddMMyyyy_HHmm')) $_AUTOMODE = 0 # ------------------------------------------------------------------------------------------------------------------------- # NOTHING BELOW THIS POINT IS REQUIRED TO BE UPDATED BY THE USER (UNLESS THEY WANT TO IMPROVE THE CODE WHICH IS APPRECIATED # MODIFIYING ANYTHING FROM THIS POINT MIGHT CAUSE THE SCRIPT TO NOT WORK # ------------------------------------------------------------------------------------------------------------------------- New-Item -ItemType "directory" -Path $_WORK_FOLDER -Name $_LOGS_FOLDER | Out-Null # HTTPutility addon # ------------------------- Add-Type -AssemblyName System.Web # Convert variable to URI compatible format # ----------------------- $_CODIFIED_API_PASSWORD = [System.Web.HttpUtility]::UrlEncode($_API_PASSWORD) # Allow SSL connections # ----------------------- add-type @" using System.Net; using System.Security.Cryptography.X509Certificates; public class TrustAllCertsPolicy : ICertificatePolicy { public bool CheckValidationResult( ServicePoint srvPoint, X509Certificate certificate, WebRequest request, int certificateProblem) { return true; } } "@ [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # Check operation to be performed # ------------------------------- if ($_OPERATION -eq $null) { Write-Host "" Write-Host "" Write-Host "ImportMAN! 1.8" Write-Host "--------------" Write-Host "" Write-Host "Helping YOU stay lazy." Write-Host "" Write-Host "" Write-Host "Type:" Write-Host "" Write-Host "1 for IPHost mode" Write-Host "2 for FQDN mode" Write-Host "3 for Auto-Mode(TM) BETA AF mode" Write-Host "4 for Sophos XG XML Splitter" Write-Host "" Write-Host "" $_OPERATION = read-host -Prompt "Please select operation mode: " switch ($_OPERATION) { 1 { $_OPERATION = "IP_Mode" } 2 { $_OPERATION = "FQDN_Mode" } 3 { $_AUTOMODE = "On" } 4 { $_OPERATION = "Splitter_Mode" } } Write-Host "" Write-Host "" } # Testing credentials prior to upload objects # ------------------------------------------- Write-Host "[INFO] Validating connection to appliance..." $_API_QUERY_URL = "https://$($_FIREWALL_IP):$($_FIREWALL_PORT)/webconsole/APIController?reqxml=<Request><Login><UserName>$($_API_USER)</UserName><Password>$($_CODIFIED_API_PASSWORD)</Password></Login></Request>" $_API_QUERY_RESULT = Invoke-WebRequest -Uri "$_API_QUERY_URL" [xml] $_API_QUERY_CONNECTIVITY_TEST_RESULT = $_API_QUERY_RESULT.Content if ($_API_QUERY_CONNECTIVITY_TEST_RESULT.Response.Login.Status -ne "Authentication Successful") { Write-Host "[WARN] A problem have occur connecting to the firewall or credentials are not valid. Check and try again later." break } else { Write-Host "[ OK ] Connection to firewall succesful. Starting object creation." Write-Host "" } # Main functions # -------------- # IP Host, FQDN and Auto mode. # ---------------------------- if ($_OPERATION -eq "IP_Mode" -or $_OPERATION -eq "FQDN_Mode" -or $_AUTOMODE -eq "On") { foreach($line in [System.IO.File]::ReadLines("$_WORK_FOLDER$_DATA_FILE_NAME")) { # Obtain and split CSV data # ------------------------ $_ITEM_NAME = ($line -split ',')[0] $_SECOND_FIELD = ($line -split ',')[1] $_THIRD_FIELD = ($line -split ',')[2] # Checking for empty line # ------------------------ if ($_ITEM_NAME -eq $nul) { Write-Host "[INFO] Skipping empty line." Continue } # Detection of type of object for AutoMode # ------------------------ if ($_AUTOMODE -eq "On") { if ($_SECOND_FIELD -eq $nul) { if ($_ITEM_NAME -match '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}') { $_OPERATION = "IP_Mode" } else { $_OPERATION = "FQDN_Mode" } } else { if ($_SECOND_FIELD -match '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}') { $_OPERATION = "IP_Mode" } else { $_OPERATION = "FQDN_Mode" } } } # Netsmak conversion from CSV data # ------------------------ if ($_OPERATION -eq "IP_Mode" -and $_SECOND_FIELD -ne $nul) { if ($_THIRD_FIELD -eq $nul) { $_THIRD_FIELD = "255.255.255.255" } else { if (($_THIRD_FIELD -eq "/32") -or ($_THIRD_FIELD -eq "32")){ $_THIRD_FIELD = "255.255.255.255" } if (($_THIRD_FIELD -eq "/31") -or ($_THIRD_FIELD -eq "31")){ $_THIRD_FIELD = "255.255.255.254" } if (($_THIRD_FIELD -eq "/30") -or ($_THIRD_FIELD -eq "30")){ $_THIRD_FIELD = "255.255.255.252" } if (($_THIRD_FIELD -eq "/29") -or ($_THIRD_FIELD -eq "29")){ $_THIRD_FIELD = "255.255.255.248" } if (($_THIRD_FIELD -eq "/28") -or ($_THIRD_FIELD -eq "28")){ $_THIRD_FIELD = "255.255.255.240" } if (($_THIRD_FIELD -eq "/27") -or ($_THIRD_FIELD -eq "27")){ $_THIRD_FIELD = "255.255.255.224" } if (($_THIRD_FIELD -eq "/26") -or ($_THIRD_FIELD -eq "26")){ $_THIRD_FIELD = "255.255.255.192" } if (($_THIRD_FIELD -eq "/25") -or ($_THIRD_FIELD -eq "25")){ $_THIRD_FIELD = "255.255.255.128" } if (($_THIRD_FIELD -eq "/24") -or ($_THIRD_FIELD -eq "24")){ $_THIRD_FIELD = "255.255.255.0" } if (($_THIRD_FIELD -eq "/23") -or ($_THIRD_FIELD -eq "23")){ $_THIRD_FIELD = "255.255.254.0" } if (($_THIRD_FIELD -eq "/22") -or ($_THIRD_FIELD -eq "22")){ $_THIRD_FIELD = "255.255.252.0" } if (($_THIRD_FIELD -eq "/21") -or ($_THIRD_FIELD -eq "21")){ $_THIRD_FIELD = "255.255.248.0" } if (($_THIRD_FIELD -eq "/20") -or ($_THIRD_FIELD -eq "20")){ $_THIRD_FIELD = "255.255.240.0" } if (($_THIRD_FIELD -eq "/19") -or ($_THIRD_FIELD -eq "19")){ $_THIRD_FIELD = "255.255.224.0" } if (($_THIRD_FIELD -eq "/18") -or ($_THIRD_FIELD -eq "18")){ $_THIRD_FIELD = "255.255.192.0" } if (($_THIRD_FIELD -eq "/17") -or ($_THIRD_FIELD -eq "17")){ $_THIRD_FIELD = "255.255.128.0" } if (($_THIRD_FIELD -eq "/16") -or ($_THIRD_FIELD -eq "16")){ $_THIRD_FIELD = "255.255.0.0" } if (($_THIRD_FIELD -eq "/15") -or ($_THIRD_FIELD -eq "15")){ $_THIRD_FIELD = "255.254.0.0" } if (($_THIRD_FIELD -eq "/14") -or ($_THIRD_FIELD -eq "14")){ $_THIRD_FIELD = "255.252.0.0" } if (($_THIRD_FIELD -eq "/13") -or ($_THIRD_FIELD -eq "13")){ $_THIRD_FIELD = "255.248.0.0" } if (($_THIRD_FIELD -eq "/12") -or ($_THIRD_FIELD -eq "12")){ $_THIRD_FIELD = "255.240.0.0" } if (($_THIRD_FIELD -eq "/11") -or ($_THIRD_FIELD -eq "11")){ $_THIRD_FIELD = "255.224.0.0" } if (($_THIRD_FIELD -eq "/10") -or ($_THIRD_FIELD -eq "10")){ $_THIRD_FIELD = "255.192.0.0" } if (($_THIRD_FIELD -eq "/9") -or ($_THIRD_FIELD -eq "9")){ $_THIRD_FIELD = "255.128.0.0" } if (($_THIRD_FIELD -eq "/8") -or ($_THIRD_FIELD -eq "8")){ $_THIRD_FIELD = "255.0.0.0" } if (($_THIRD_FIELD -eq "/7") -or ($_THIRD_FIELD -eq "7")){ $_THIRD_FIELD = "254.0.0.0" } if (($_THIRD_FIELD -eq "/6") -or ($_THIRD_FIELD -eq "6")){ $_THIRD_FIELD = "252.0.0.0" } if (($_THIRD_FIELD -eq "/5") -or ($_THIRD_FIELD -eq "5")){ $_THIRD_FIELD = "248.0.0.0" } if (($_THIRD_FIELD -eq "/4") -or ($_THIRD_FIELD -eq "4")){ $_THIRD_FIELD = "240.0.0.0" } if (($_THIRD_FIELD -eq "/3") -or ($_THIRD_FIELD -eq "3")){ $_THIRD_FIELD = "224.0.0.0" } if (($_THIRD_FIELD -eq "/2") -or ($_THIRD_FIELD -eq "2")){ $_THIRD_FIELD = "192.0.0.0" } if (($_THIRD_FIELD -eq "/1") -or ($_THIRD_FIELD -eq "1")){ $_THIRD_FIELD = "128.0.0.0" } if (($_THIRD_FIELD -eq "/0") -or ($_THIRD_FIELD -eq "0")){ $_THIRD_FIELD = "0.0.0.0" } } } if ($_OPERATION -eq "IP_Mode" -and $_SECOND_FIELD -eq $nul) { $_SECOND_FIELD = $_ITEM_NAME $_THIRD_FIELD = "255.255.255.255" } # Operations for FQDN selection # ----------------------------- if ($_OPERATION -eq "FQDN_Mode") { if ($_SECOND_FIELD -eq $nul) { $_SECOND_FIELD = $_ITEM_NAME } } # Prefix for name of object # ------------------------- if ($_ADD_PREFIXES_TO_OBJECTS -eq "Yes") { if ($_OPERATION -eq "FQDN_Mode") { $_ITEM_NAME = "FQDN_" + $_ITEM_NAME } else { if ($_THIRD_FIELD -eq "255.255.255.255") { $_ITEM_NAME = "HOST_" + $_ITEM_NAME } else { $_ITEM_NAME = "NET_" + $_ITEM_NAME } } } # Convert CSV variable to URI compatible format # --------------------------------------------- $_CODIFIED_ITEM_NAME = [System.Web.HttpUtility]::UrlEncode($_ITEM_NAME) $_CODIFIED_ITEM_NAME = ($_CODIFIED_ITEM_NAME -replace "%26","%26amp;") # API Query # --------- # For IPHost item # ---------------- if ($_OPERATION -eq "IP_Mode") { if($_THIRD_FIELD -eq "255.255.255.255") { $_API_QUERY_URL = "https://$($_FIREWALL_IP):$($_FIREWALL_PORT)/webconsole/APIController?reqxml=<Request><Login><UserName>$($_API_USER)</UserName><Password>$($_CODIFIED_API_PASSWORD)</Password></Login><Set><IPHost><Name>$($_CODIFIED_ITEM_NAME)</Name><IPFamily>IPv4</IPFamily><HostType>IP</HostType><IPAddress>$($_SECOND_FIELD)</IPAddress></IPHost></Set></Request>" $_TYPE_OBJECT = "Host" } else { $_API_QUERY_URL = "https://$($_FIREWALL_IP):$($_FIREWALL_PORT)/webconsole/APIController?reqxml=<Request><Login><UserName>$($_API_USER)</UserName><Password>$($_CODIFIED_API_PASSWORD)</Password></Login><Set><IPHost><Name>$($_CODIFIED_ITEM_NAME)</Name><IPFamily>IPv4</IPFamily><HostType>Network</HostType><IPAddress>$($_SECOND_FIELD)</IPAddress><Subnet>$($_THIRD_FIELD)</Subnet></IPHost></Set></Request>" $_TYPE_OBJECT = "Network" } } # For FQDN item # -------------- if ($_OPERATION -eq "FQDN_Mode") { $_API_QUERY_URL = "https://$($_FIREWALL_IP):$($_FIREWALL_PORT)/webconsole/APIController?reqxml=<Request><Login><UserName>$($_API_USER)</UserName><Password>$($_CODIFIED_API_PASSWORD)</Password></Login><Set><FQDNHost><Name>$($_CODIFIED_ITEM_NAME)</Name><FQDN>$($_SECOND_FIELD)</FQDN></FQDNHost></Set></Request>" $_TYPE_OBJECT = "FQDN" } Write-Host "" Write-Host "[INFO] Processing $($_TYPE_OBJECT)" Write-Host "[INFO] Name: $($_ITEM_NAME)" Write-Host "[INFO] Value: $($_SECOND_FIELD)" $_API_QUERY_RESULT = Invoke-WebRequest -Uri "$_API_QUERY_URL" [xml] $_API_QUERY_RESULT_PARSED = $_API_QUERY_RESULT.Content # Check result # ------------ $_LINE_LOG = [String](Get-Date) + "," + "[$($_TYPE_OBJECT)]" + "," + $_ITEM_NAME if ($_OPERATION -eq "IP_Mode") { $_API_OPERATION_RESULT = $_API_QUERY_RESULT_PARSED.Response.IPHost.Status } if ($_OPERATION -eq "FQDN_Mode") { $_API_OPERATION_RESULT = $_API_QUERY_RESULT_PARSED.Response.FQDNHost.Status } if ($_API_OPERATION_RESULT.code -eq 200) { echo "[ OK ] CODE: $($_API_OPERATION_RESULT.code) - Processed OK" $_LINE_LOG | Out-File -Append -Encoding utf8 -FilePath "$($_WORK_FOLDER)$($_LOGS_FOLDER)\OK.txt" } else { echo "[WARN] CODE: $($_API_OPERATION_RESULT.code) - Error in execution $($_API_OPERATION_RESULT.'#text')" $_LINE_LOG | Out-File -Append -Encoding utf8 -FilePath "$($_WORK_FOLDER)$($_LOGS_FOLDER)\NO_OK.txt" } } } # FIN IP Host, FQDN and Auto mode. # -------------------------------- # XML Splitter # ------------ if ($_OPERATION -eq "Splitter_Mode") { rv _API_XML_URL | Out-Null Write-Host "Splitter Mode allows to take an existing Sophos XG XML export and load it" Write-Host "one by one, having visual feedback when each object loads without waiting for" Write-Host "the whole result to come." Write-Host "" Write-Host "Example of normal XML object: " Write-Host "" Write-Host '<IPHost transactionid="">' Write-Host ' <Name>172.20.1.16/32</Name>' Write-Host ' <IPFamily>IPv4</IPFamily>' Write-Host ' <HostType>IP</HostType>' Write-Host ' <IPAddress>172.20.1.16</IPAddress>' Write-Host '</IPHost>' Write-Host "" Write-Host "In this example, the tag for this object is IPHost. If you have many of this, just" Write-Host "put the XML in the file, indicate the tag to look for and wait for the automated" Write-Host "loading." Write-Host "" $_TAG_TO_IMPORT = read-host -Prompt "Please write the tag to search for: " Write-Host "" Write-Host "" foreach($line in [System.IO.File]::ReadLines("$_WORK_FOLDER$_DATA_FILE_NAME")) { $_API_XML_URL = $_API_XML_URL + $line if($line -match '<Name>') { $_ITEM_NAME = $line -replace '<[/]?Name>','' } if ($line -match "</$($_TAG_TO_IMPORT)>") { Write-Host "[INFO] Processing $($_ITEM_NAME)" $_API_XML_URL_CLEANED = $_API_XML_URL -replace "> *<","><" $_API_QUERY_URL = "https://$($_FIREWALL_IP):$($_FIREWALL_PORT)/webconsole/APIController?reqxml=<Request><Login><UserName>$($_API_USER)</UserName><Password>$($_CODIFIED_API_PASSWORD)</Password></Login><Set>$($_API_XML_URL_CLEANED)</Set></Request>" $_API_QUERY_RESULT = Invoke-WebRequest -Uri "$_API_QUERY_URL" [xml] $_API_QUERY_RESULT_PARSED = $_API_QUERY_RESULT.Content if ($_API_QUERY_RESULT.Content -match "<Status code=`"200`"") { Write-Host "[ OK ] Processed OK" } else { Write-Host "[WARN] Error in execution" } rv _API_XML_URL | Out-Null Write-Host "" } } } # FIN XML Splitter # ------------
You need to download this code and put in somewhere your Windows computer as a .ps1 Powershell file. Then you have to open "Windows Powershell ISE" in administrator mode and open the script in there.
Now, go to your firewall and allow API usage from the IP that will have the script running:
- Enable API
- Type the IP address of the computer hosting the script
- Add it
- Apply
Once you have opened the script, you need to supply the parameters for your firewall:
- The IP of your firewall
- The admin port of your firewall
- The admin user (normally "admin")
- The password for the account
- The name of the file including the objects (needs to be inside the "work folder")
- The work folder (any folder as long as the script can write there)
- If you want to add prefixes to objects created by script (HOST_, NET_, FQDN_ - Have to put "Yes" or "No")
Run It
And then, run it!
You'll be prompted with the execution mode as explained before. Be careful, if you select "IPHost" but you have FQDN in your file, it might cause problems or errors.
Choose your mode and press ENTER. The program will start checking every object and proceed to create it in the firewall and provide output about that operation. If the code is 200, everything is ok. Otherwise, check the code in the Sophos API documentation.
The script also creates a log file in the working folder for future references about what failed and what ended up OK.
Whats next?
There're some know issues:
- If you mess up the password, it will fail every item in the file instead of saying it at the beginning, working on it.
- Trying to add more ways for automode to accept FQDN (check for *?, check for 255?, etc).
- Lacking some arguments to run it in non interactive mode (faster?)
- Will try to add more things that it can automate (services?, iphostgroups?)
And yeah, basically this is a hobby for fun but if you have doubts, requests, bugs, let me know. I'll try to address them.
Take care, stay safe!
----
Updated 21 November, Version 1.5:
- Fixed some things (FQDN detection)
- Added credential validation
Updated March 5th, Version 1.6:
- Fixed log folder problem
- Added context help on code
Updated May 11th, Version 1.7:
- Added case for single IP detection on auto mode
- Added option to add prefixes to objects while adding them
Updated May 15th, Version 1.8:
-
- Added splitter mode
- Some code cleanup.
Updated April 8th:
- This will be no longer be updated or monitored.
Added TAGs
[edited by: Raphael Alganes at 5:26 AM (GMT -7) on 18 Sep 2024]