Skip to content

Instantly share code, notes, and snippets.

@sylr
Last active June 22, 2021 14:08
Show Gist options
  • Save sylr/cbf9d145b153f325c5a5a9d50ab24785 to your computer and use it in GitHub Desktop.
Save sylr/cbf9d145b153f325c5a5a9d50ab24785 to your computer and use it in GitHub Desktop.
Fortigate config address group flattening script
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# This file is based on github.com/maaaaz/fgpoliciestocsv
#
# Copyright (C) 2021, Sylvain Rabot <sylvain at abstraction .fr>
# Copyright (C) 2014, 2020, Thomas Debize <tdebize at mail.com>
# All rights reserved.
#
# This script is a free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# fgpoliciestocsv is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with fgpoliciestocsv. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from os import path
import io, time
import sys
import re
import os
import json
from netaddr import IPAddress
# OptionParser imports
from optparse import OptionParser
from optparse import OptionGroup
# Options definition
parser = OptionParser(usage="%prog [options]")
main_grp = OptionGroup(parser, 'Main parameters')
main_grp.add_option('-i', '--input-file', help='Partial or full Fortigate configuration file. Ex: fgfw.cfg')
main_grp.add_option('-e', '--encoding', help='Input file encoding (default "utf8")', default='utf8')
parser.option_groups.extend([main_grp])
# Python 2 and 3 compatibility
if (sys.version_info < (3, 0)):
fd_read_options = 'r'
fd_write_options = 'wb'
else:
fd_read_options = 'r'
fd_write_options = 'w'
# Handful patterns
# -- Entering group definition block
def parse(options):
# -- Entering group definition block
p_entering_group_block = re.compile(r'^\s*config firewall addrgrp$', re.IGNORECASE)
# -- Exiting group definition block
p_exiting_group_block = re.compile(r'^end$', re.IGNORECASE)
# -- Commiting the current group definition and going to the next one
p_group_next = re.compile(r'^next$', re.IGNORECASE)
# -- Policy number
p_group_name = re.compile(r'^\s*edit\s+"(?P<group_name>.*)"$', re.IGNORECASE)
# -- Policy setting
p_group_set = re.compile(r'^\s*set\s+(?P<group_key>\S+)\s+(?P<group_value>.*)$', re.IGNORECASE)
in_group = False
groups = {}
group = {}
with io.open(options.input_file, mode=fd_read_options, encoding=options.encoding) as fd_input:
for line in fd_input:
line = line.strip()
# edit <group_name>
if p_group_name.search(line):
in_group = True
group_name = p_group_name.search(line).group('group_name')
group['name'] = group_name
# set <group_key> <group_value>
if p_group_set.search(line):
group_key = p_group_set.search(line).group('group_key')
group_value = parse_group_value(p_group_set.search(line).group('group_value').strip(), group_key, group_name)
group[group_key] = group_value
# next or end
if in_group and (p_group_next.search(line) or p_exiting_group_block.search(line)):
in_group = False
if 'name' in group:
groups[group['name']] = group
group = {}
# flatten group members
for key in groups:
#print("#0 {0}".format(key), file=sys.stderr)
if 'member' in groups[key]:
groups[key]['member_flattened'] = []
for member in groups[key]['member']:
#print(" #1 {0}".format(member), file=sys.stderr)
groups[key]['member_flattened'] = groups[key]['member_flattened'] + recursive_members(member, groups)
json.dump(groups, sys.stdout, indent=4, sort_keys=True)
return
def recursive_members(name, groups):
#print(" #1 {0}".format(name), file=sys.stderr)
subnets = []
if str(name) not in groups:
print('ERROR: group name {0} does not exists'.format(name), file=sys.stderr)
return subnets
if 'member' in groups[name]:
for member in groups[name]['member']:
subnets = subnets + recursive_members(member, groups)
elif 'subnet' in groups[name]:
subnets = subnets + groups[name]['subnet']
return subnets
def parse_group_value(value: str, group_key: str, group_name: str) -> list[str]:
multi_value = []
in_value = False
index_value_start = 0
escape_current_char = False
for i in range(len(value)):
if escape_current_char:
escape_current_char = False
continue
elif value[i] == '"':
if not in_value:
in_value = True
index_value_start = i+1
continue
else:
string = value[index_value_start:i]
if group_key == 'subnet':
ip, mask = string.split(" ", 2)
string = "{0}/{1} # {2}".format(ip, IPAddress(mask).netmask_bits(), group_name)
multi_value.append(string)
in_value = False
index_value_start = 0
elif value[i] == '\\':
escape_current_char = True
elif i == 0:
string = value
if group_key == 'subnet':
ip, mask = string.split(" ", 2)
string = "{0}/{1} # {2}".format(ip, IPAddress(mask).netmask_bits(), group_name)
return [string]
return multi_value
def main():
global parser
options, _ = parser.parse_args()
if (options.input_file == None):
parser.error('Please specify a valid input file')
parse(options)
return None
if __name__ == "__main__" :
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment