# Title: Fortigate ACL Report Generator # Description: Takes the Firewall Config file as input and parses out the ACL rules to facilitate Firewall Reviews and Reporting. # This verbose version also includes the underlying IP addresses from Address groups and aliases. # Specify the Fortigate configuration file to parse $InputFile = "$home\Downloads\MY_FIREWALL_CONFIG_CHANGE_ME.conf" # Define recursive function for determining the address from an Alias function TranslateAddressAlias($ParameterList) { $AddressList = $ParameterList[0] $AddressAlias = $ParameterList[1] $DepthCount = $ParameterList[2] $TranslatedAddress = "" # Ensure recursion doesn't go indefinitely $DepthCount += 1 if ($DepthCount -gt 50) { Return "Error: Recursion too deep" } # Look in the Address list for the Address Alias $LookupValues = $AddressList | Where-Object {$_.AddressAlias -eq $AddressAlias} # Check if we found something $LookupMeasure = $LookupValues | measure if ($LookupMeasure.Count -ne 0) { $TranslatedAddress += $AddressAlias + " (" # We found the translated address. Loop since there may be multiple values foreach ($value in $LookupValues) { if ($value.AddressAlias -eq $value.Address) { # If the values are the same, return the value to prevent infinite recusion $ReturnArray = $value.Address } else { # Recursively look to see if this is an Alias $ReturnArray = TranslateAddressAlias($AddressList, $Value.Address, $DepthCount) } if ($LookupMeasure.Count -eq 1) { # If this is an Address Alias just append the value (avoids extra paranthesis) $TranslatedAddress += $ReturnArray } else { # There are multiple values so enclose them in an extra set of paranthesis $TranslatedAddress += "(" + $ReturnArray + ") " } } $TranslatedAddress = $TranslatedAddress.TrimEnd() $TranslatedAddress += ")" } else { # There is no alias for this value, so return the passed in value. Trim tailing whitespace $TranslatedAddress = $AddressAlias.TrimEnd() } $ReturnValue = $TranslatedAddress.TrimEnd() Return $ReturnValue } # Main Report Generation Logic # Initialize the reporting array and variables $ACLInfo = "" | select PolicyID, PolicyName, SrcInt, DstInt, SrcAddr, DstAddr, Action, Service, Comments, Status $AddressInfo = "" | select AddressAlias, Address $ACLReport = @() $AddressLookup = @() $HostName = "UNKNOWN" $AddressName = "" # Setup StreamReader for the Fortigate Config File [System.IO.StreamReader] $ConfigStreamReader = New-Object ` -TypeName 'System.IO.StreamReader' ` -ArgumentList ($InputFile, $true) # Read through each line of the config file try { $FoundAddressGroups = $false $FoundAddresses = $false $FoundACLs = $false $ReadAllACLs = $false # Assumes config is in this order: Addresses, Address Groups, ACL config while ($ReadAllACLs -eq $false) { $CurrentLine = $ConfigStreamReader.ReadLine() # Look for the Fortgate HostName if ($CurrentLine.StartsWith(' set hostname ')) { $HostName = $CurrentLine.Substring(17, $CurrentLine.Length - 17) } # Block for Address parsing configuration if ($FoundAddresses -eq $false) { if ($CurrentLine.Contains('config firewall address')) { # We found the Address Group configuration section $FoundAddresses = $true } } else { # Start parsing the address group section if ($CurrentLine.Equals('end')) { # This is the end of the Adress Group configuration section. $FoundAddresses = $false } else { # Here we are parsing through the address config section $TrimmedLine = $CurrentLine.TrimStart() # Check if this is the start of a new policy if ($TrimmedLine.StartsWith('edit ')) { $AddressInfo.AddressAlias = $TrimmedLine.Substring(5, $TrimmedLine.Length -5).Trim('"') } # Check if this is the subnet if ($TrimmedLine.StartsWith('set subnet ')) { $AddressInfo.Address = $TrimmedLine.Substring(11, $TrimmedLine.Length -11).Trim('"') $AddressLookup += $AddressInfo.PSObject.Copy() # Use Copy() function otherwise only the reference is copied } # Check if this is the fqdn if ($TrimmedLine.StartsWith('set fqdn ')) { $AddressInfo.Address = $TrimmedLine.Substring(9, $TrimmedLine.Length -9).Trim('"') $AddressLookup += $AddressInfo.PSObject.Copy() # Use Copy() function otherwise only the reference is copied } # Check if this is the start-ip if ($TrimmedLine.StartsWith('set start-ip ')) { $AddressInfo.Address = $TrimmedLine.Substring(13, $TrimmedLine.Length -13).Trim('"') + "-" #$AddressLookup += $AddressInfo.PSObject.Copy() # Use Copy() function otherwise only the reference is copied } # Check if this is the end-ip if ($TrimmedLine.StartsWith('set end-ip ')) { $AddressInfo.Address += $TrimmedLine.Substring(11, $TrimmedLine.Length -11).Trim('"') $AddressLookup += $AddressInfo.PSObject.Copy() # Use Copy() function otherwise only the reference is copied } } } # Block for Address Group parsing configuration if ($FoundAddressGroups -eq $false) { if ($CurrentLine.Contains('config firewall addrgrp')) { # We found the Address Group configuration section $FoundAddressGroups = $true } } else { # Start parsing the address group section if ($CurrentLine.Equals('end')) { # This is the end of the Adress Group configuration section. $FoundAddressGroups = $false } else { # Here we are parsing through the address alias config section $TrimmedLine = $CurrentLine.TrimStart() # Check if this is the start of a new policy if ($TrimmedLine.StartsWith('edit ')) { $AddressName = $TrimmedLine.Substring(5, $TrimmedLine.Length -5) } # Check if this is one of the addresses in the address group (might be an alias) if ($TrimmedLine.StartsWith('set member ')) { $MemberLine = $TrimmedLine.Substring(11, $TrimmedLine.Length -11).Replace('" ',',').Replace('"', '') # There may be multiple addresses here, separated by a space $Members = $MemberLine.Split(',') # Add a new row for each address foreach ($Member in $Members) { $AddressInfo.AddressAlias = $AddressName.Replace('"', '') $AddressInfo.Address = $Member.Trim() $AddressLookup += $AddressInfo.PSObject.Copy() # Use Copy() function otherwise only the reference is copied } } } } # Block for ACL parsing configuration if ($FoundACLs -eq $false) { # We haven't yet found the ACL configuration if ($CurrentLine.Contains('config firewall policy')) { # We found the ACL configuration section - Huzzah! $FoundACLs = $true } } else { # We are currently in the ACL configuration section. Look for the end of this section if ($CurrentLine.Equals('end')) { # This is the end of the ACL configuration section. No need to look any further at this file $ReadAllACLs = $true } else { # Here we are parsing through the config section $TrimmedLine = $CurrentLine.TrimStart() #$TrimmedLine = $TrimmedLine.Replace('"', '') # Check if this is the start of a new policy if ($TrimmedLine.StartsWith('edit ')) { $ACLInfo.PolicyID = $TrimmedLine.Substring(5, $TrimmedLine.Length -5) $ACLInfo.Action = "deny" # Unless action is specifically allowed, it is denied $ACLInfo.Status = "enabled" # Unless status is specifically set, it is enabled } # check if this is the end of a policy if ($TrimmedLine.Equals('next')) { # Write the data to our master report object $ACLReport += $ACLInfo.PSObject.Copy() # Use Copy() function otherwise only the reference is copied # Clean out of the temp object for the next policy $ACLInfo.PolicyID = '' $ACLInfo.PolicyName = '' $ACLInfo.SrcInt = '' $ACLInfo.DstInt = '' $ACLInfo.SrcAddr = '' $ACLInfo.DstAddr = '' $ACLInfo.Action = '' $ACLInfo.Service = '' $ACLInfo.Comments = '' $ACLInfo.Status = '' } # Check if this is the name if ($TrimmedLine.StartsWith('set name ')) { $ACLInfo.PolicyName = $TrimmedLine.Substring(9, $TrimmedLine.Length -9).Replace('" ', ', ').Replace('"','') } # Check if this is the Source Interface if ($TrimmedLine.StartsWith('set srcintf ')) { $ACLInfo.SrcInt = $TrimmedLine.Substring(12, $TrimmedLine.Length -12).Replace('" ', ', ').Replace('"','') } # Check if this is the Destination Interface if ($TrimmedLine.StartsWith('set dstintf ')) { $ACLInfo.DstInt = $TrimmedLine.Substring(12, $TrimmedLine.Length -12).Replace('" ', ', ').Replace('"','') } # Check if this is the Source Address if ($TrimmedLine.StartsWith('set srcaddr ')) { #$ACLInfo.SrcAddr = $TrimmedLine.Substring(12, $TrimmedLine.Length -12).Replace('" ', ', ').Replace('"','') $TranslatedSources = "" $ListOfSources = $TrimmedLine.Substring(12, $TrimmedLine.Length -12).Replace('" ', ',').Replace('"','') $SourceArray = $ListOfSources.Split(',') foreach ($source in $SourceArray) { $TranslatedSources += TranslateAddressAlias($AddressLookup, $source, 0) $TranslatedSources += ", " } $AclInfo.SrcAddr = $TranslatedSources.TrimEnd(", ") } # Check if this is the Destination Address if ($TrimmedLine.StartsWith('set dstaddr ')) { #$ACLInfo.DstAddr = $TrimmedLine.Substring(12, $TrimmedLine.Length -12).Replace('" ', ', ').Replace('"','') $TranslatedDest = "" $ListOfDests = $TrimmedLine.Substring(12, $TrimmedLine.Length -12).Replace('" ', ',').Replace('"','') $DestArray = $ListOfDests.Split(',') foreach ($dest in $DestArray) { $TranslatedDest += TranslateAddressAlias($AddressLookup, $dest, 0) $TranslatedDest += ", " } $ACLInfo.DstAddr = $TranslatedDest.TrimEnd(", ") } # If Internet-Service is defined, set as Destination Address if ($TrimmedLine.StartsWith('set internet-service-name ')) { $ACLInfo.DstAddr = $TrimmedLine.Substring(26, $TrimmedLine.Length -26).Replace('" ', ', ').Replace('"','') $ACLInfo.Service = "Internet Services" # The service is specific to the "Internet Service" used } # Check if this is the Action if ($TrimmedLine.StartsWith('set action ')) { $ACLInfo.Action = $TrimmedLine.Substring(11, $TrimmedLine.Length -11).Replace('" ', ', ').Replace('"','') } # Check if this is the Service if ($TrimmedLine.StartsWith('set service ')) { $ACLInfo.Service = $TrimmedLine.Substring(12, $TrimmedLine.Length -12).Replace('" ', ', ').Replace('"','') } # Check if this is the Action if ($TrimmedLine.StartsWith('set comments ')) { $ACLInfo.Comments = $TrimmedLine.Substring(13, $TrimmedLine.Length -13).Replace('" ', ', ').Replace('"','') } if ($TrimmedLine.equals('set status disable')) { $ACLInfo.Status = "disabled" } } } } } Catch { $ErrorText = "Error occurred during file parsing: " + $_ Write-Output $ErrorText Exit } finally { $ConfigStreamReader.Close(); } # Build output file path, appending with current timestamp, in the current user's Downloads directory $Now = Get-Date -Format "yyyyMMddHHmm" $HostName = $HostName.Trim('"') $ReportFileName = "$home\Downloads\Fortigate_ACL_Report_" + $HostName + "_" + $Now + ".csv" # Don't do any Sort operations on the report # The ACL rules will be sorted based on sequence within the Config File. # Write the master report out to a file $ACLReport | Export-CSV -Path $ReportFileName -NoTypeInformation # Output a success message to the screen $Message = "File generated successfully: " + $ReportFileName Write-Output $Message