Update segenxml to include support for templated booleans and tunables

The segenxml tool is used to generate documentation regarding the policy
definitions. Its output is an XML file that contains the in-line
comments associated with boolean generation as well as interface
definitions.

With booleans also generated inside templates, this information was
(until now) ignored. Templates such as apache's apache_content_template
which created new booleans were not properly documented, as the
in-template comments were ignored.

In this patch, we will go over module code first and seek template
calls. When a template call is matched, the module code is updated
(expanded) with the template content (while substituting the arguments
to get a proper code listing). Only after all templates have been
expanded we seek the necessary boolean definitions.

Changes since v2:
- Fix BOOLEAN statements to match backtick (`) and tick (') usages as
  well
- Fix match for arguments to also include multiple entries ( { ... } )

Changes since v1:
- Also apply the regexp on BOOLEAN to allow generating templated
  boolean/tunable documentation

Signed-off-by: Sven Vermeulen <sven.vermeulen@siphos.be>
This commit is contained in:
Sven Vermeulen 2018-03-25 13:56:36 +02:00 committed by Chris PeBenito
parent 9d8bb4eb93
commit 744482a3e6
1 changed files with 69 additions and 10 deletions

View File

@ -43,7 +43,8 @@ INTERFACE = re.compile(r"^\s*(interface|template)\(`(\w*)'")
# -> ("bool", "secure_mode", "false")
# "gen_tunable(allow_kerberos, false)"
# -> ("tunable", "allow_kerberos", "false")
BOOLEAN = re.compile(r"^\s*gen_(tunable|bool)\(\s*(\w*)\s*,\s*(true|false)\s*\)")
BOOLEAN = re.compile(r"^\s*gen_(tunable|bool)\(\s*\`?\s*(\w*)\s*\'?\s*,\s*(true|false)\s*\)")
TEMPLATE_BOOLEAN = re.compile(r"^\s*gen_(tunable|bool)\(\s*\`?\s*([\w\$]*)\s*\'?\s*,\s*(true|false)\s*\)")
# Matches a XML comment in the policy, which is defined as any line starting
# with two # and at least one character of white space. Will give the single
@ -54,8 +55,16 @@ BOOLEAN = re.compile(r"^\s*gen_(tunable|bool)\(\s*(\w*)\s*,\s*(true|false)\s*\)"
# -> ("<summary>")
# "## The domain allowed access. "
# -> ("The domain allowed access.")
XML_COMMENT = re.compile(r"^##\s+(.*?)\s*$")
XML_COMMENT = re.compile(r"^\s*##\s+(.*?)\s*$")
# Matches a template call in the policy, which is defined as any line having
# a function call like structure, being a string, followed by a set of
# arguments between an opening and closing bracket. Regexp cannot deal with
# unknown number of arguments, so we will split arguments in the code later on.
# Some examples:
# "userdom_user_access_template(gpg, gpg_t)"
# "zarafa_domain_template(gateway)"
TEMPLATE_CALL = re.compile(r"^\s*(\w*_template)\(\s*(\w*)\s*(?:,\s*(?:[^,)]*)\s*)*\)")
# FUNCTIONS
def getModuleXML(file_name):
@ -164,7 +173,13 @@ def getModuleXML(file_name):
interface = None
continue
# If the line is a boolean/tunable definition, ignore it for now (these
# lines are processed later on) and dismiss the XML comment received
# thus far as it is otherwise attributed to an interface.
tunable = TEMPLATE_BOOLEAN.match(line)
if tunable:
temp_buf = []
continue
# If the file just had a header, add the comments to the module buffer.
if finding_header:
@ -197,6 +212,49 @@ def getTunableXML(file_name, kind):
tunable_buf = []
temp_buf = []
tunable_processed_code = []
# We first go through the code and substitute template calls with the
# complete template content. This needs to happen iteratively, because
# a template can call another template. In order to ensure no cyclic
# template calls keep us busy, we max out at 9999 substitutions
has_changed = True
subst_threshold = 9999
while (has_changed and (subst_threshold > 0)):
has_changed = False
for line in tunable_code:
# Get the template call match
template_call = TEMPLATE_CALL.match(line)
# If we reach a template call, read in the template data
# from the template directory, but substitute all $1 with
# the second match, $2 with the third match, etc.
if template_call:
# Read template file based on template_call.group(1)
try:
template_file = open(templatedir + "/" + template_call.group(1) + ".iftemplate", "r")
template_code = template_file.readlines()
template_file.close()
except OSError:
warning("cannot open file %s for read, bailing out" % templatedir + "/" + template_call.group(1) + ".iftemplate")
return []
# Substitute content (i.e. $1 for argument 1, $2 for argument 2, etc.)
template_split = re.findall(r"[\w\" {}]+", line.strip())
for index, item in enumerate(template_code):
for group in range(1, len(template_split)):
template_code[index] = template_code[index].replace("$" + str(group), template_split[group].strip())
# Now 'inject' the code in the tunable_code variable
tunable_processed_code.extend(template_code)
has_changed = True
subst_threshold -= 1
else:
tunable_processed_code.append(line)
# It is a bad practice to try and update lists while in a loop, so we
# created an intermediate one and are now assigning it back
tunable_code = tunable_processed_code
tunable_processed_code = []
# If subst_threshold is 0 or less we want to know
if (subst_threshold <= 0):
warning("Detected a possible loop in policy code and template usage")
# Find tunables and booleans line by line and use the comments above
# them.
@ -251,14 +309,15 @@ def usage():
Displays a message describing the proper usage of this script.
"""
sys.stdout.write("usage: %s [-w] [-mtb] <file>\n\n" % sys.argv[0])
sys.stdout.write("usage: %s [-w] [-T <templatedir>] [-mtb] <file>\n\n" % sys.argv[0])
sys.stdout.write("-w --warn\t\t\tshow warnings\n"+\
"-m --module <file>\t\tname of module to process\n"+\
"-t --tunable <file>\t\tname of global tunable file to process\n"+\
"-b --boolean <file>\t\tname of global boolean file to process\n\n")
"-b --boolean <file>\t\tname of global boolean file to process\n"+\
"-T --templates <dir>\t\tname of template directory to use\n\n")
sys.stdout.write("examples:\n")
sys.stdout.write("> %s -w -m policy/modules/apache\n" % sys.argv[0])
sys.stdout.write("> %s -w -T tmp/templates -m policy/modules/apache\n" % sys.argv[0])
sys.stdout.write("> %s -t policy/global_tunables\n" % sys.argv[0])
def warning(description):
@ -289,6 +348,7 @@ warn = False
module = False
tunable = False
boolean = False
templatedir = ''
# Check that there are command line arguments.
if len(sys.argv) <= 1:
@ -297,7 +357,7 @@ if len(sys.argv) <= 1:
# Parse command line args
try:
opts, args = getopt.getopt(sys.argv[1:], 'whm:t:b:', ['warn', 'help', 'module=', 'tunable=', 'boolean='])
opts, args = getopt.getopt(sys.argv[1:], 'whm:t:b:T:', ['warn', 'help', 'module=', 'tunable=', 'boolean=', 'templates='])
except getopt.GetoptError:
usage()
sys.exit(2)
@ -309,13 +369,12 @@ for o, a in opts:
sys.exit(0)
elif o in ('-m', '--module'):
module = a
break
elif o in ('-t', '--tunable'):
tunable = a
break
elif o in ('-b', '--boolean'):
boolean = a
break
elif o in ('-T', '--templates'):
templatedir = a
else:
usage()
sys.exit(2)