From 2447d0d511f1a387570387d0bc026a4c3a5993c1 Mon Sep 17 00:00:00 2001 From: Leo Date: Thu, 14 Dec 2023 21:29:28 +0100 Subject: [PATCH] Feature/improve pre commit (#119) --- .pre-commit-config.yaml | 13 ++++ .../iosxeSTIG/callback_plugins/stig_xml.py | 65 ++++++++++-------- .../rhel7STIG/callback_plugins/stig_xml.py | 67 ++++++++++--------- .../rhel8STIG/callback_plugins/stig_xml.py | 67 ++++++++++--------- .../win2022STIG/callback_plugins/stig_xml.py | 67 ++++++++++--------- 5 files changed, 160 insertions(+), 119 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1f0b27b84..196f849ee 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,6 +5,15 @@ repos: hooks: - id: end-of-file-fixer - id: trailing-whitespace + + - id: check-yaml + exclude: \.j2.(yaml|yml)$|\.(yaml|yml).j2$ + args: [--unsafe] # see https://github.com/pre-commit/pre-commit-hooks/issues/273 + + - id: check-toml + - id: check-json + - id: check-symlinks + - repo: https://github.com/ansible/ansible-lint.git # get latest release tag from https://github.com/ansible/ansible-lint/releases/ rev: v6.20.3 @@ -13,4 +22,8 @@ repos: additional_dependencies: - jmespath + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 23.11.0 + hooks: + - id: black ... diff --git a/collections/ansible_collections/demo/compliance/roles/iosxeSTIG/callback_plugins/stig_xml.py b/collections/ansible_collections/demo/compliance/roles/iosxeSTIG/callback_plugins/stig_xml.py index 456527301..bedc176cd 100644 --- a/collections/ansible_collections/demo/compliance/roles/iosxeSTIG/callback_plugins/stig_xml.py +++ b/collections/ansible_collections/demo/compliance/roles/iosxeSTIG/callback_plugins/stig_xml.py @@ -1,4 +1,5 @@ -from __future__ import (absolute_import, division, print_function) +from __future__ import absolute_import, division, print_function + __metaclass__ = type from ansible.plugins.callback import CallbackBase @@ -14,61 +15,65 @@ role = "iosxeSTIG" + class CallbackModule(CallbackBase): CALLBACK_VERSION = 2.0 - CALLBACK_TYPE = 'xml' - CALLBACK_NAME = 'stig_xml' + CALLBACK_TYPE = "xml" + CALLBACK_NAME = "stig_xml" CALLBACK_NEEDS_WHITELIST = True def __init__(self): super(CallbackModule, self).__init__() self.rules = {} - self.stig_path = os.environ.get('STIG_PATH') - self.XML_path = os.environ.get('XML_PATH') + self.stig_path = os.environ.get("STIG_PATH") + self.XML_path = os.environ.get("XML_PATH") if self.stig_path is None: self.stig_path = os.path.join(os.getcwd(), "roles", role, "files") - self._display.display('Using STIG_PATH: {}'.format(self.stig_path)) + self._display.display("Using STIG_PATH: {}".format(self.stig_path)) if self.XML_path is None: self.XML_path = os.getcwd() - self._display.display('Using XML_PATH: {}'.format(self.XML_path)) + self._display.display("Using XML_PATH: {}".format(self.XML_path)) print("Writing: {}".format(self.XML_path)) STIG_name = os.path.basename(self.stig_path) - ET.register_namespace('cdf', 'http://checklists.nist.gov/xccdf/1.2') - self.tr = ET.Element('{http://checklists.nist.gov/xccdf/1.2}TestResult') - self.tr.set('id', 'xccdf_mil.disa.stig_testresult_scap_mil.disa_comp_{}'.format(STIG_name)) + ET.register_namespace("cdf", "http://checklists.nist.gov/xccdf/1.2") + self.tr = ET.Element("{http://checklists.nist.gov/xccdf/1.2}TestResult") + self.tr.set( + "id", + "xccdf_mil.disa.stig_testresult_scap_mil.disa_comp_{}".format(STIG_name), + ) endtime = strftime("%Y-%m-%dT%H:%M:%S", gmtime()) - self.tr.set('end-time', endtime) - tg = ET.SubElement(self.tr, '{http://checklists.nist.gov/xccdf/1.2}target') + self.tr.set("end-time", endtime) + tg = ET.SubElement(self.tr, "{http://checklists.nist.gov/xccdf/1.2}target") tg.text = platform.node() def __get_rev(self, nid): - rev = '0' + rev = "0" # Check all files for the rule number. for file in os.listdir(self.stig_path): - with open(os.path.join(self.stig_path, file), 'r') as f: - r = 'SV-{}r(?P\d)_rule'.format(nid) + with open(os.path.join(self.stig_path, file), "r") as f: + r = "SV-{}r(?P\d)_rule".format(nid) m = re.search(r, f.read()) if m: - rev = m.group('rev') + rev = m.group("rev") break return rev def v2_runner_on_ok(self, result): name = result._task.get_name() - m = re.search('stigrule_(?P\d+)', name) + m = re.search("stigrule_(?P\d+)", name) if m: - nid = m.group('id') + nid = m.group("id") else: return rev = self.__get_rev(nid) key = "{}r{}".format(nid, rev) - if self.rules.get(key, 'Unknown') != False: + if self.rules.get(key, "Unknown") != False: self.rules[key] = result.is_changed() def __set_duplicates(self): - with open(os.path.join(self.stig_path, 'duplicates.json')) as f: + with open(os.path.join(self.stig_path, "duplicates.json")) as f: dups = json.load(f) for d in dups: dup_of = str(dups[d][0]) @@ -82,17 +87,19 @@ def __set_duplicates(self): def v2_playbook_on_stats(self, stats): self.__set_duplicates() for rule, changed in self.rules.items(): - state = 'fail' if changed else 'pass' - rr = ET.SubElement(self.tr, '{http://checklists.nist.gov/xccdf/1.2}rule-result') - rr.set('idref', 'xccdf_mil.disa.stig_rule_SV-{}_rule'.format(rule)) - rs = ET.SubElement(rr, '{http://checklists.nist.gov/xccdf/1.2}result') + state = "fail" if changed else "pass" + rr = ET.SubElement( + self.tr, "{http://checklists.nist.gov/xccdf/1.2}rule-result" + ) + rr.set("idref", "xccdf_mil.disa.stig_rule_SV-{}_rule".format(rule)) + rs = ET.SubElement(rr, "{http://checklists.nist.gov/xccdf/1.2}result") rs.text = state passing = len(self.rules) - sum(self.rules.values()) - sc = ET.SubElement(self.tr, '{http://checklists.nist.gov/xccdf/1.2}score') - sc.set('maximum', str(len(self.rules))) - sc.set('system', 'urn:xccdf:scoring:flat-unweighted') + sc = ET.SubElement(self.tr, "{http://checklists.nist.gov/xccdf/1.2}score") + sc.set("maximum", str(len(self.rules))) + sc.set("system", "urn:xccdf:scoring:flat-unweighted") sc.text = str(passing) - with open(os.path.join(self.XML_path, "xccdf-results.xml"), 'w') as f: + with open(os.path.join(self.XML_path, "xccdf-results.xml"), "w") as f: out = ET.tostring(self.tr) - pretty = xml.dom.minidom.parseString(out).toprettyxml(encoding='utf-8') + pretty = xml.dom.minidom.parseString(out).toprettyxml(encoding="utf-8") f.write(pretty) diff --git a/collections/ansible_collections/demo/compliance/roles/rhel7STIG/callback_plugins/stig_xml.py b/collections/ansible_collections/demo/compliance/roles/rhel7STIG/callback_plugins/stig_xml.py index cfff078b2..5474b8316 100644 --- a/collections/ansible_collections/demo/compliance/roles/rhel7STIG/callback_plugins/stig_xml.py +++ b/collections/ansible_collections/demo/compliance/roles/rhel7STIG/callback_plugins/stig_xml.py @@ -1,4 +1,5 @@ -from __future__ import (absolute_import, division, print_function) +from __future__ import absolute_import, division, print_function + __metaclass__ = type from ansible.plugins.callback import CallbackBase @@ -11,76 +12,82 @@ import xml.etree.ElementTree as ET import xml.dom.minidom + class CallbackModule(CallbackBase): CALLBACK_VERSION = 2.0 - CALLBACK_TYPE = 'xml' - CALLBACK_NAME = 'stig_xml' + CALLBACK_TYPE = "xml" + CALLBACK_NAME = "stig_xml" CALLBACK_NEEDS_WHITELIST = True def _get_STIG_path(self): - cwd = os.path.abspath('.') + cwd = os.path.abspath(".") for dirpath, dirs, files in os.walk(cwd): - if os.path.sep + 'files' in dirpath and '.xml' in files[0]: + if os.path.sep + "files" in dirpath and ".xml" in files[0]: return os.path.join(cwd, dirpath, files[0]) def __init__(self): super(CallbackModule, self).__init__() self.rules = {} - self.stig_path = os.environ.get('STIG_PATH') - self.XML_path = os.environ.get('XML_PATH') + self.stig_path = os.environ.get("STIG_PATH") + self.XML_path = os.environ.get("XML_PATH") if self.stig_path is None: self.stig_path = self._get_STIG_path() - self._display.display('Using STIG_PATH: {}'.format(self.stig_path)) + self._display.display("Using STIG_PATH: {}".format(self.stig_path)) if self.XML_path is None: self.XML_path = tempfile.mkdtemp() + "/xccdf-results.xml" - self._display.display('Using XML_PATH: {}'.format(self.XML_path)) + self._display.display("Using XML_PATH: {}".format(self.XML_path)) print("Writing: {}".format(self.XML_path)) STIG_name = os.path.basename(self.stig_path) - ET.register_namespace('cdf', 'http://checklists.nist.gov/xccdf/1.2') - self.tr = ET.Element('{http://checklists.nist.gov/xccdf/1.2}TestResult') - self.tr.set('id', 'xccdf_mil.disa.stig_testresult_scap_mil.disa_comp_{}'.format(STIG_name)) + ET.register_namespace("cdf", "http://checklists.nist.gov/xccdf/1.2") + self.tr = ET.Element("{http://checklists.nist.gov/xccdf/1.2}TestResult") + self.tr.set( + "id", + "xccdf_mil.disa.stig_testresult_scap_mil.disa_comp_{}".format(STIG_name), + ) endtime = strftime("%Y-%m-%dT%H:%M:%S", gmtime()) - self.tr.set('end-time', endtime) - tg = ET.SubElement(self.tr, '{http://checklists.nist.gov/xccdf/1.2}target') + self.tr.set("end-time", endtime) + tg = ET.SubElement(self.tr, "{http://checklists.nist.gov/xccdf/1.2}target") tg.text = platform.node() def _get_rev(self, nid): - with open(self.stig_path, 'r') as f: - r = 'SV-{}r(?P\d+)_rule'.format(nid) + with open(self.stig_path, "r") as f: + r = "SV-{}r(?P\d+)_rule".format(nid) m = re.search(r, f.read()) if m: - rev = m.group('rev') + rev = m.group("rev") else: - rev = '0' + rev = "0" return rev def v2_runner_on_ok(self, result): name = result._task.get_name() - m = re.search('stigrule_(?P\d+)', name) + m = re.search("stigrule_(?P\d+)", name) if m: - nid = m.group('id') + nid = m.group("id") else: return rev = self._get_rev(nid) key = "{}r{}".format(nid, rev) - if self.rules.get(key, 'Unknown') != False: + if self.rules.get(key, "Unknown") != False: self.rules[key] = result.is_changed() def v2_playbook_on_stats(self, stats): for rule, changed in self.rules.items(): - state = 'fail' if changed else 'pass' - rr = ET.SubElement(self.tr, '{http://checklists.nist.gov/xccdf/1.2}rule-result') - rr.set('idref', 'xccdf_mil.disa.stig_rule_SV-{}_rule'.format(rule)) - rs = ET.SubElement(rr, '{http://checklists.nist.gov/xccdf/1.2}result') + state = "fail" if changed else "pass" + rr = ET.SubElement( + self.tr, "{http://checklists.nist.gov/xccdf/1.2}rule-result" + ) + rr.set("idref", "xccdf_mil.disa.stig_rule_SV-{}_rule".format(rule)) + rs = ET.SubElement(rr, "{http://checklists.nist.gov/xccdf/1.2}result") rs.text = state passing = len(self.rules) - sum(self.rules.values()) - sc = ET.SubElement(self.tr, '{http://checklists.nist.gov/xccdf/1.2}score') - sc.set('maximum', str(len(self.rules))) - sc.set('system', 'urn:xccdf:scoring:flat-unweighted') + sc = ET.SubElement(self.tr, "{http://checklists.nist.gov/xccdf/1.2}score") + sc.set("maximum", str(len(self.rules))) + sc.set("system", "urn:xccdf:scoring:flat-unweighted") sc.text = str(passing) - with open(self.XML_path, 'wb') as f: + with open(self.XML_path, "wb") as f: out = ET.tostring(self.tr) - pretty = xml.dom.minidom.parseString(out).toprettyxml(encoding='utf-8') + pretty = xml.dom.minidom.parseString(out).toprettyxml(encoding="utf-8") f.write(pretty) diff --git a/collections/ansible_collections/demo/compliance/roles/rhel8STIG/callback_plugins/stig_xml.py b/collections/ansible_collections/demo/compliance/roles/rhel8STIG/callback_plugins/stig_xml.py index cfff078b2..5474b8316 100644 --- a/collections/ansible_collections/demo/compliance/roles/rhel8STIG/callback_plugins/stig_xml.py +++ b/collections/ansible_collections/demo/compliance/roles/rhel8STIG/callback_plugins/stig_xml.py @@ -1,4 +1,5 @@ -from __future__ import (absolute_import, division, print_function) +from __future__ import absolute_import, division, print_function + __metaclass__ = type from ansible.plugins.callback import CallbackBase @@ -11,76 +12,82 @@ import xml.etree.ElementTree as ET import xml.dom.minidom + class CallbackModule(CallbackBase): CALLBACK_VERSION = 2.0 - CALLBACK_TYPE = 'xml' - CALLBACK_NAME = 'stig_xml' + CALLBACK_TYPE = "xml" + CALLBACK_NAME = "stig_xml" CALLBACK_NEEDS_WHITELIST = True def _get_STIG_path(self): - cwd = os.path.abspath('.') + cwd = os.path.abspath(".") for dirpath, dirs, files in os.walk(cwd): - if os.path.sep + 'files' in dirpath and '.xml' in files[0]: + if os.path.sep + "files" in dirpath and ".xml" in files[0]: return os.path.join(cwd, dirpath, files[0]) def __init__(self): super(CallbackModule, self).__init__() self.rules = {} - self.stig_path = os.environ.get('STIG_PATH') - self.XML_path = os.environ.get('XML_PATH') + self.stig_path = os.environ.get("STIG_PATH") + self.XML_path = os.environ.get("XML_PATH") if self.stig_path is None: self.stig_path = self._get_STIG_path() - self._display.display('Using STIG_PATH: {}'.format(self.stig_path)) + self._display.display("Using STIG_PATH: {}".format(self.stig_path)) if self.XML_path is None: self.XML_path = tempfile.mkdtemp() + "/xccdf-results.xml" - self._display.display('Using XML_PATH: {}'.format(self.XML_path)) + self._display.display("Using XML_PATH: {}".format(self.XML_path)) print("Writing: {}".format(self.XML_path)) STIG_name = os.path.basename(self.stig_path) - ET.register_namespace('cdf', 'http://checklists.nist.gov/xccdf/1.2') - self.tr = ET.Element('{http://checklists.nist.gov/xccdf/1.2}TestResult') - self.tr.set('id', 'xccdf_mil.disa.stig_testresult_scap_mil.disa_comp_{}'.format(STIG_name)) + ET.register_namespace("cdf", "http://checklists.nist.gov/xccdf/1.2") + self.tr = ET.Element("{http://checklists.nist.gov/xccdf/1.2}TestResult") + self.tr.set( + "id", + "xccdf_mil.disa.stig_testresult_scap_mil.disa_comp_{}".format(STIG_name), + ) endtime = strftime("%Y-%m-%dT%H:%M:%S", gmtime()) - self.tr.set('end-time', endtime) - tg = ET.SubElement(self.tr, '{http://checklists.nist.gov/xccdf/1.2}target') + self.tr.set("end-time", endtime) + tg = ET.SubElement(self.tr, "{http://checklists.nist.gov/xccdf/1.2}target") tg.text = platform.node() def _get_rev(self, nid): - with open(self.stig_path, 'r') as f: - r = 'SV-{}r(?P\d+)_rule'.format(nid) + with open(self.stig_path, "r") as f: + r = "SV-{}r(?P\d+)_rule".format(nid) m = re.search(r, f.read()) if m: - rev = m.group('rev') + rev = m.group("rev") else: - rev = '0' + rev = "0" return rev def v2_runner_on_ok(self, result): name = result._task.get_name() - m = re.search('stigrule_(?P\d+)', name) + m = re.search("stigrule_(?P\d+)", name) if m: - nid = m.group('id') + nid = m.group("id") else: return rev = self._get_rev(nid) key = "{}r{}".format(nid, rev) - if self.rules.get(key, 'Unknown') != False: + if self.rules.get(key, "Unknown") != False: self.rules[key] = result.is_changed() def v2_playbook_on_stats(self, stats): for rule, changed in self.rules.items(): - state = 'fail' if changed else 'pass' - rr = ET.SubElement(self.tr, '{http://checklists.nist.gov/xccdf/1.2}rule-result') - rr.set('idref', 'xccdf_mil.disa.stig_rule_SV-{}_rule'.format(rule)) - rs = ET.SubElement(rr, '{http://checklists.nist.gov/xccdf/1.2}result') + state = "fail" if changed else "pass" + rr = ET.SubElement( + self.tr, "{http://checklists.nist.gov/xccdf/1.2}rule-result" + ) + rr.set("idref", "xccdf_mil.disa.stig_rule_SV-{}_rule".format(rule)) + rs = ET.SubElement(rr, "{http://checklists.nist.gov/xccdf/1.2}result") rs.text = state passing = len(self.rules) - sum(self.rules.values()) - sc = ET.SubElement(self.tr, '{http://checklists.nist.gov/xccdf/1.2}score') - sc.set('maximum', str(len(self.rules))) - sc.set('system', 'urn:xccdf:scoring:flat-unweighted') + sc = ET.SubElement(self.tr, "{http://checklists.nist.gov/xccdf/1.2}score") + sc.set("maximum", str(len(self.rules))) + sc.set("system", "urn:xccdf:scoring:flat-unweighted") sc.text = str(passing) - with open(self.XML_path, 'wb') as f: + with open(self.XML_path, "wb") as f: out = ET.tostring(self.tr) - pretty = xml.dom.minidom.parseString(out).toprettyxml(encoding='utf-8') + pretty = xml.dom.minidom.parseString(out).toprettyxml(encoding="utf-8") f.write(pretty) diff --git a/collections/ansible_collections/demo/compliance/roles/win2022STIG/callback_plugins/stig_xml.py b/collections/ansible_collections/demo/compliance/roles/win2022STIG/callback_plugins/stig_xml.py index cfff078b2..5474b8316 100644 --- a/collections/ansible_collections/demo/compliance/roles/win2022STIG/callback_plugins/stig_xml.py +++ b/collections/ansible_collections/demo/compliance/roles/win2022STIG/callback_plugins/stig_xml.py @@ -1,4 +1,5 @@ -from __future__ import (absolute_import, division, print_function) +from __future__ import absolute_import, division, print_function + __metaclass__ = type from ansible.plugins.callback import CallbackBase @@ -11,76 +12,82 @@ import xml.etree.ElementTree as ET import xml.dom.minidom + class CallbackModule(CallbackBase): CALLBACK_VERSION = 2.0 - CALLBACK_TYPE = 'xml' - CALLBACK_NAME = 'stig_xml' + CALLBACK_TYPE = "xml" + CALLBACK_NAME = "stig_xml" CALLBACK_NEEDS_WHITELIST = True def _get_STIG_path(self): - cwd = os.path.abspath('.') + cwd = os.path.abspath(".") for dirpath, dirs, files in os.walk(cwd): - if os.path.sep + 'files' in dirpath and '.xml' in files[0]: + if os.path.sep + "files" in dirpath and ".xml" in files[0]: return os.path.join(cwd, dirpath, files[0]) def __init__(self): super(CallbackModule, self).__init__() self.rules = {} - self.stig_path = os.environ.get('STIG_PATH') - self.XML_path = os.environ.get('XML_PATH') + self.stig_path = os.environ.get("STIG_PATH") + self.XML_path = os.environ.get("XML_PATH") if self.stig_path is None: self.stig_path = self._get_STIG_path() - self._display.display('Using STIG_PATH: {}'.format(self.stig_path)) + self._display.display("Using STIG_PATH: {}".format(self.stig_path)) if self.XML_path is None: self.XML_path = tempfile.mkdtemp() + "/xccdf-results.xml" - self._display.display('Using XML_PATH: {}'.format(self.XML_path)) + self._display.display("Using XML_PATH: {}".format(self.XML_path)) print("Writing: {}".format(self.XML_path)) STIG_name = os.path.basename(self.stig_path) - ET.register_namespace('cdf', 'http://checklists.nist.gov/xccdf/1.2') - self.tr = ET.Element('{http://checklists.nist.gov/xccdf/1.2}TestResult') - self.tr.set('id', 'xccdf_mil.disa.stig_testresult_scap_mil.disa_comp_{}'.format(STIG_name)) + ET.register_namespace("cdf", "http://checklists.nist.gov/xccdf/1.2") + self.tr = ET.Element("{http://checklists.nist.gov/xccdf/1.2}TestResult") + self.tr.set( + "id", + "xccdf_mil.disa.stig_testresult_scap_mil.disa_comp_{}".format(STIG_name), + ) endtime = strftime("%Y-%m-%dT%H:%M:%S", gmtime()) - self.tr.set('end-time', endtime) - tg = ET.SubElement(self.tr, '{http://checklists.nist.gov/xccdf/1.2}target') + self.tr.set("end-time", endtime) + tg = ET.SubElement(self.tr, "{http://checklists.nist.gov/xccdf/1.2}target") tg.text = platform.node() def _get_rev(self, nid): - with open(self.stig_path, 'r') as f: - r = 'SV-{}r(?P\d+)_rule'.format(nid) + with open(self.stig_path, "r") as f: + r = "SV-{}r(?P\d+)_rule".format(nid) m = re.search(r, f.read()) if m: - rev = m.group('rev') + rev = m.group("rev") else: - rev = '0' + rev = "0" return rev def v2_runner_on_ok(self, result): name = result._task.get_name() - m = re.search('stigrule_(?P\d+)', name) + m = re.search("stigrule_(?P\d+)", name) if m: - nid = m.group('id') + nid = m.group("id") else: return rev = self._get_rev(nid) key = "{}r{}".format(nid, rev) - if self.rules.get(key, 'Unknown') != False: + if self.rules.get(key, "Unknown") != False: self.rules[key] = result.is_changed() def v2_playbook_on_stats(self, stats): for rule, changed in self.rules.items(): - state = 'fail' if changed else 'pass' - rr = ET.SubElement(self.tr, '{http://checklists.nist.gov/xccdf/1.2}rule-result') - rr.set('idref', 'xccdf_mil.disa.stig_rule_SV-{}_rule'.format(rule)) - rs = ET.SubElement(rr, '{http://checklists.nist.gov/xccdf/1.2}result') + state = "fail" if changed else "pass" + rr = ET.SubElement( + self.tr, "{http://checklists.nist.gov/xccdf/1.2}rule-result" + ) + rr.set("idref", "xccdf_mil.disa.stig_rule_SV-{}_rule".format(rule)) + rs = ET.SubElement(rr, "{http://checklists.nist.gov/xccdf/1.2}result") rs.text = state passing = len(self.rules) - sum(self.rules.values()) - sc = ET.SubElement(self.tr, '{http://checklists.nist.gov/xccdf/1.2}score') - sc.set('maximum', str(len(self.rules))) - sc.set('system', 'urn:xccdf:scoring:flat-unweighted') + sc = ET.SubElement(self.tr, "{http://checklists.nist.gov/xccdf/1.2}score") + sc.set("maximum", str(len(self.rules))) + sc.set("system", "urn:xccdf:scoring:flat-unweighted") sc.text = str(passing) - with open(self.XML_path, 'wb') as f: + with open(self.XML_path, "wb") as f: out = ET.tostring(self.tr) - pretty = xml.dom.minidom.parseString(out).toprettyxml(encoding='utf-8') + pretty = xml.dom.minidom.parseString(out).toprettyxml(encoding="utf-8") f.write(pretty)