From 1d26006afd480ecb79317b16dc2135e8e4a8e2b6 Mon Sep 17 00:00:00 2001 From: rk89gc Date: Tue, 12 Jan 2021 13:04:03 +0100 Subject: [PATCH] - Nmap scan support for multiple XML files - gets enabled if 'OpenPorts' directive is found in the template - FIX: Fixed deprecated functions e.g. icon/OnFileDrop handling - Added support for multiple charts in the _xml_apply_chart functions via objects cloning. - Support for page titles in coreProperties - automatically generated if not set with Test.DocumentTitle - FIX: Template filename is now resolved with abspath fixing the "empty directory" error - FIX: Empty scan files directive in CLI would give a NoneType error, changed to an empty array --- README.md | 8 +- cxfreeze_setup.py | 2 +- src/cli.py | 11 ++- src/gui.py | 105 +++++++++++++++++++-- src/report.py | 228 +++++++++++++++++++++++++++++++++++++++++----- src/version.py | 12 ++- src/yamled.py | 6 +- 7 files changed, 331 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 0ec790b..e3bcf93 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,8 @@ Main application window contains four fields that act as an input - Scan - HP WebInspect / Burp Suite Pro scan - Knowledge base - knowledge base that could be used to reinforce final report customization +- Nmap scan - parse XML output files from Nmap scans and include them + in the report Double click on given text area will popup the content on larger area. @@ -55,14 +57,14 @@ Command-line support has been added in order to allow bulk generation of report-files. Application currently supports one set of switches: ``` --t template-file [-c content-file] [-k kb-file] [-s scan-file] --r report-file +-t template-file [-c content-file] [-k kb-file] [-s scan-file] [-n nmap-file] +-r report-file [-o output-content-file] ``` Example use: ``` -python report-ng.py -t examples/example-2-scan-report-template.xml -c examples/example-2-content.yaml -k examples/example-2-kb.yaml -s examples/example-2-scan-export-Burp.xml -r examples/\!.xml +python report-ng.py -t examples/example-2-scan-report-template.xml -c examples/example-2-content.yaml -k examples/example-2-kb.yaml -s examples/example-2-scan-export-Burp.xml -r examples/\!.xml -o examples/\!.yaml ``` ## Word Template Preparation diff --git a/cxfreeze_setup.py b/cxfreeze_setup.py index 705b01f..c439db5 100644 --- a/cxfreeze_setup.py +++ b/cxfreeze_setup.py @@ -2,7 +2,7 @@ # Dependencies are automatically detected, but it might need # fine tuning. -buildOptions = dict(packages=['lxml._elementpath', 'gzip'], excludes=[], compressed=True) +buildOptions = dict(packages=['lxml._elementpath', 'gzip'], excludes=['collections.abc'], compressed=True) import sys base = 'Win32GUI' if sys.platform=='win32' else None diff --git a/src/cli.py b/src/cli.py index 8cfada9..6103927 100644 --- a/src/cli.py +++ b/src/cli.py @@ -39,7 +39,7 @@ def value (key): def values (key): if key in sys.argv: return map(lambda x: sys.argv[x + 1], filter(lambda x: x + 1 < len(sys.argv), filter(lambda x: sys.argv[x] == key, range(len(sys.argv))))) - return None + return [] #None caused error if empty. def is_csv (filename): ext = '.csv' @@ -53,6 +53,7 @@ def is_yaml (filename): content_file = value('-c') kb_file = value('-k') scan_files = values('-s') + nmap_files = values('-n') report_file = value('-r') output_file = value('-o') summary_file = value('-u') @@ -60,17 +61,23 @@ def is_yaml (filename): if template_file and report_file: report = Report() report.template_load_xml(template_file) + if content_file: if is_yaml(content_file): report.content_load_yaml(content_file) else: report.content_load_json(content_file) + for scan_file in scan_files: #report.scan = Scan(scan_file) scan = Scan(scan_file) report.merge_scan(scan.modify()) report.content_refresh() del scan + + for nmap_file in nmap_files: + report.nmap_load_xml(nmap_file) + if kb_file: if is_csv(kb_file): report.kb_load_csv(kb_file) @@ -112,7 +119,7 @@ def is_yaml (filename): else: print 'Usage: ' print - print ' ' + self.title + '.exe -t template-file [-c content-file] [-k kb-file] [[-s scan-file] ...] -r output-report-file [-o output-content-file]' + print ' ' + self.title + '.exe -t template-file [-c content-file] [-k kb-file] [[-s scan-file] ...] [[-n nmap-file] ...] -r output-report-file [-o output-content-file]' print ' generate report (and optionally - content as yaml / json)' print print ' ' + self.title + '.exe -s scan-file -o output-scan-file' diff --git a/src/gui.py b/src/gui.py index a6ff251..f43d04a 100755 --- a/src/gui.py +++ b/src/gui.py @@ -134,6 +134,9 @@ def next(self): self.menu_file_open_k = menu_file.Append(index.next(), 'Open &Knowledge Base...') self.menu_file_open_k.Enable(False) self.Bind(wx.EVT_MENU, self.Open_Knowledge_Base, id=index.current) + self.menu_file_open_m = menu_file.Append(index.next(), 'Open &Nmap Scan...') + self.menu_file_open_m.Enable(False) + self.Bind(wx.EVT_MENU, self.Open_Nmap_Scan, id=index.current) #menu_file.AppendSeparator() #self.menu_file_generate_c = menu_file.Append(index.next(), '&Generate Content') #self.menu_file_generate_c.Enable(False) @@ -205,6 +208,11 @@ def next(self): self.menu_tools_merge_kb_into_content = menu_tools.Append(index.next(), 'Merge KB into Content') self.menu_tools_merge_kb_into_content.Enable(False) self.Bind(wx.EVT_MENU, self.Merge_KB_Into_Content, id=index.current) + # remove nmap + self.menu_tools_remove_nmap_scan = menu_tools.Append(index.next(), 'Remove Nmap Scan') + self.menu_tools_remove_nmap_scan.Enable(False) + self.Bind(wx.EVT_MENU, self.Remove_Nmap_Scan, id=index.current) + self.menu_tools_generate_few_passwords = menu_tools.Append(index.next(), 'Generate &few passwords') self.Bind(wx.EVT_MENU, self.Generate_few_passwords, id=index.current) @@ -230,7 +238,7 @@ def __init__(self, target, handler): self.target = target self.handler = handler def OnDropFiles(self, x, y, filenames): - self.handler(filenames) + return self.handler(filenames) panel = wx.Panel(self) vbox = wx.BoxSizer(wx.VERTICAL) fgs = wx.FlexGridSizer(5, 2, 9, 25) @@ -260,8 +268,9 @@ def ctrl_tc_t_OnMouseOver(e): def ctrl_tc_t_OnDropFiles(filenames): if len(filenames) != 1: wx.MessageBox('Single file is expected!', 'Error', wx.OK | wx.ICON_ERROR) - return + return False self._open_template(filenames[0]) + return True ctrl_tc_t_dt = FileDropTarget(self.ctrl_tc_t, ctrl_tc_t_OnDropFiles) self.ctrl_tc_t.SetDropTarget(ctrl_tc_t_dt) fgs.AddMany([(self.ctrl_st_t, 1, wx.EXPAND), (self.ctrl_tc_t, 1, wx.EXPAND)]) @@ -314,10 +323,12 @@ def ctrl_tc_c_OnMouseOver(e): def ctrl_tc_c_OnDropFiles(filenames): if len(filenames) != 1: wx.MessageBox('Single file is expected!', 'Error', wx.OK | wx.ICON_ERROR) - return + return False self._open_content(filenames[0]) if self.ctrl_st_c.IsEnabled(): # Yamled self.ctrl_tc_c_b.Show() + + return True ctrl_tc_c_dt = FileDropTarget(self.ctrl_tc_c, ctrl_tc_c_OnDropFiles) self.ctrl_tc_c.SetDropTarget(ctrl_tc_c_dt) fgs.AddMany([(self.ctrl_st_c, 1, wx.EXPAND), (self.ctrl_tc_c, 1, wx.EXPAND)]) @@ -367,10 +378,12 @@ def ctrl_tc_s_OnMouseOver(e): def ctrl_tc_s_OnDropFiles(filenames): if len(filenames) != 1: wx.MessageBox('Single file is expected!', 'Error', wx.OK | wx.ICON_ERROR) - return + return False self._open_scan(filenames[0]) if self.ctrl_st_s.IsEnabled(): # Yamled self.ctrl_tc_s_b.Show() + + return False ctrl_tc_s_dt = FileDropTarget(self.ctrl_tc_s, ctrl_tc_s_OnDropFiles) self.ctrl_tc_s.SetDropTarget(ctrl_tc_s_dt) fgs.AddMany([(self.ctrl_st_s, 1, wx.EXPAND), (self.ctrl_tc_s, 1, wx.EXPAND)]) @@ -401,11 +414,46 @@ def ctrl_tc_k_OnMouseLeave(e): def ctrl_tc_k_OnDropFiles(filenames): if len(filenames) != 1: wx.MessageBox('Single file is expected!', 'Error', wx.OK | wx.ICON_ERROR) - return + return False self._open_kb(filenames[0]) + return True ctrl_tc_k_dt = FileDropTarget(self.ctrl_tc_k, ctrl_tc_k_OnDropFiles) self.ctrl_tc_k.SetDropTarget(ctrl_tc_k_dt) fgs.AddMany([(self.ctrl_st_k, 1, wx.EXPAND), (self.ctrl_tc_k, 1, wx.EXPAND)]) + + # Nmap Scan + self.ctrl_st_n = wx.StaticText(panel, label='Nmap Scan:') + self.ctrl_st_n.Enable(False) + self.ctrl_tc_n = wx.TextCtrl(panel, style=wx.TE_MULTILINE | wx.TE_READONLY, size=(200, 3 * 17,)) + self.ctrl_tc_n.Enable(False) + self.ctrl_tc_n.SetBackgroundColour(self.color_tc_bg_d) + def ctrl_tc_n_OnFocus(e): + self.ctrl_tc_n.ShowNativeCaret(False) + e.Skip() + def ctrl_tc_n_OnDoubleclick(e): + if self.ctrl_st_n.IsEnabled(): + self.application.TextWindow(self, title='Nmap Scan Preview', content=self.ctrl_tc_n.GetValue()) + e.Skip() + self.ctrl_tc_n.Bind(wx.EVT_SET_FOCUS, ctrl_tc_n_OnFocus) + self.ctrl_tc_n.Bind(wx.EVT_LEFT_DCLICK, ctrl_tc_n_OnDoubleclick) + def ctrl_tc_n_OnMouseOver(e): + self.status('You might use drag & drop', hint=True) + e.Skip() + def ctrl_tc_n_OnMouseLeave(e): + self.status('') + e.Skip() + self.ctrl_tc_n.Bind(wx.EVT_ENTER_WINDOW, ctrl_tc_n_OnMouseOver) + self.ctrl_tc_n.Bind(wx.EVT_LEAVE_WINDOW, ctrl_tc_n_OnMouseLeave) + def ctrl_tc_n_OnDropFiles(filenames): + if self.ctrl_st_n.IsEnabled(): + for scan in filenames: + self._open_nmap_scan(scan) + return True + + ctrl_tc_n_dt = FileDropTarget(self.ctrl_tc_n, ctrl_tc_n_OnDropFiles) + self.ctrl_tc_n.SetDropTarget(ctrl_tc_n_dt) + fgs.AddMany([(self.ctrl_st_n, 1, wx.EXPAND), (self.ctrl_tc_n, 1, wx.EXPAND)]) + def panel_OnMouseOver(e): self.status('') self.ctrl_tc_c_b.Hide() @@ -542,6 +590,12 @@ def _open_template(self, filename): self.ctrl_tc_t.SetValue('') self.ctrl_st_c.Enable(False) self.ctrl_tc_c.SetValue('') + + # nmap + self.ctrl_st_n.Enable(False) + self.ctrl_tc_n.SetValue('') + self.ctrl_tc_n.Enable(False) + self.menu_file_open_k.Enable(False) self.menu_file_save_t.Enable(False) self.menu_file_save_r.Enable(False) @@ -570,6 +624,11 @@ def _open_template(self, filename): self.menu_tools_merge_scan_into_content.Enable(True) if self.ctrl_st_k.IsEnabled(): self.menu_tools_merge_kb_into_content.Enable(True) + + if self.report.is_template_with_nmap(): + self.ctrl_st_n.Enable(True) + self.ctrl_tc_n.Enable(True) + self.ctrl_tc_n.SetBackgroundColour(self.color_tc_bg_e) self.status('Template loaded') def Open_Content(self, e): @@ -656,6 +715,11 @@ def Merge_KB_Into_Content(self, e): self.menu_tools_merge_kb_into_content.Enable(False) self.status('Merged') + def Remove_Nmap_Scan(self, e): + self.report.nmap_remove() + self.ctrl_tc_n.SetValue('') + self.status('Removed') + def Generate_few_passwords(self, e): self.application.TextWindow(self, title='Random Password Generator', content='\n'.join(pwgen.Few(15)), size=(235, 270,)) @@ -670,7 +734,7 @@ def Generate_few_passwords(self, e): def Save_Template_As(self, e): openFileDialog = wx.FileDialog(self, 'Save Template As', self.save_into_directory, '', 'Content files (*.yaml; *.json)|*.yaml;*.json|All files (*.*)|*.*', - wx.FD_SAVE | wx.wx.FD_OVERWRITE_PROMPT) + wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) if openFileDialog.ShowModal() == wx.ID_CANCEL: return json_ext = '.json' @@ -687,7 +751,7 @@ def Save_Template_As(self, e): def Save_Content_As(self, e): openFileDialog = wx.FileDialog(self, 'Save Content As', self.save_into_directory, '', 'Content files (*.yaml; *.json)|*.yaml;*.json|All files (*.*)|*.*', - wx.FD_SAVE | wx.wx.FD_OVERWRITE_PROMPT) + wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) if openFileDialog.ShowModal() == wx.ID_CANCEL: return json_ext = '.json' @@ -703,7 +767,7 @@ def Save_Content_As(self, e): def Save_Scan_As(self, e): openFileDialog = wx.FileDialog(self, 'Save Scan As', self.save_into_directory, '', 'Content files (*.yaml; *.json)|*.yaml;*.json|All files (*.*)|*.*', - wx.FD_SAVE | wx.wx.FD_OVERWRITE_PROMPT) + wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) if openFileDialog.ShowModal() == wx.ID_CANCEL: return json_ext = '.json' @@ -732,6 +796,9 @@ def _clean_template(self, force=False): self.report.content_reload() if self.ctrl_st_k.IsEnabled(): self.report.kb_reload() + if self.ctrl_st_n.IsEnabled(): + self.report.nmap_reload() + self._refresh() # print 'cleanup performed.' #else: @@ -745,7 +812,7 @@ def Clean_template(self, e): def Save_Report_As(self, e): openFileDialog = wx.FileDialog(self, 'Save Report As', self.save_into_directory, '', 'XML files (*.xml)|*.xml|All files (*.*)|*.*', - wx.FD_SAVE | wx.wx.FD_OVERWRITE_PROMPT) + wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) if openFileDialog.ShowModal() == wx.ID_CANCEL: return filename = openFileDialog.GetPath() @@ -850,6 +917,26 @@ def _open_kb(self, filename): self.menu_tools_merge_kb_into_content.Enable(True) self.menu_tools_merge_kb_into_content.Enable(True) + def Open_Nmap_Scan(self, e): + openFileDialog = wx.FileDialog(self, 'Open Nmap Scan', '', '', + 'Nmap XML output file (*.xml)|*.xml;|All files (*.*)|*.*', + wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) + if openFileDialog.ShowModal() == wx.ID_CANCEL: + return + self._open_nmap_scan(openFileDialog.GetPath()) + + def _open_nmap_scan(self, filename): + self.ctrl_st_n.Enable(False) + result = self.report.nmap_load_xml(filename) + if result == True: + self.ctrl_tc_n.SetValue(self.report.nmap_dump()) + self.menu_tools_remove_nmap_scan.Enable(True) + else: + self.ctrl_tc_n.SetValue("") + wx.MessageBox('There was an error parsing the file!', 'Error', + wx.OK | wx.ICON_ERROR) + self.ctrl_st_n.Enable(True) + class YamledWindowWrapper(YamledWindow): def __init__(self, parent=None, title='', content=None, size=(800, 600,), *args, **kwargs): diff --git a/src/report.py b/src/report.py index bb67f47..0c621c3 100755 --- a/src/report.py +++ b/src/report.py @@ -30,6 +30,10 @@ from cgi import escape #import base64 #import docx + +import copy +from datetime import datetime + docx = None @@ -62,6 +66,8 @@ class ns: _kb_filename = None _kb_type = None _kb = None + _nmap = None + _nmap_filenames = [] _openxml = None _html_parser = None _ihtml_parser = None @@ -70,6 +76,7 @@ class ns: #__vulnparam_highlighting = True #__truncate = True _pPr_annotation = False + chartIterator = 0 def __init__(self): self._meta_init() @@ -105,7 +112,9 @@ def _cleanup(self): if self._kb: del self._kb self._kb = None - + if self._nmap: + del self._nmap + self._nmap = None @staticmethod def _dump_json(target): return json.dumps(target, indent=2, ensure_ascii=False) #.decode('utf-8') @@ -208,7 +217,6 @@ def leaf(skel): self._skel['Findings'][0][i] = self._skel['Finding'][i] del self._skel['Finding'][i] del self._skel['Finding'] - #print self._skel def template_dump_json(self): return self._dump_json(self._skel) @@ -238,6 +246,9 @@ def template_load_xml(self, filename, clean=False): self._template_filename = filename return self.template_reload(clean=clean) + def is_template_with_nmap(self): + return 'OpenPorts' in self._skel + #def template_load_docx(self, filename, clean=False): # self._docx = docx.Document(docx=filename) # self._xml = self._docx._document_part._element @@ -620,9 +631,9 @@ def submerge(block, knowledge, path): #print self._dump_yaml(self._content) del kb return self._content - + def _xml_apply_findings(self): - finding_struct = filter(lambda x: x[0] == ['Finding'], self._struct) + finding_struct = filter(lambda x: x[0] == ['Finding'], self._struct) #the whole Finding object in Word template (including Factors/Description etc.) #print map(lambda x: x['Name'], self._meta['Findings']) findings = self._meta['Findings'] for severity in self.severity.keys(): @@ -685,20 +696,45 @@ def _xml_apply_findings(self): # is this finding.[severity]? -> if yes, replace content, otherwise delete me if i['struct'][-1] in map(lambda x: self._severity_tag(x)+'?', self.severity.keys()): if i['struct'][-1] == severity_tag+'?': - self._xml_sdt_replace(i['sdt'], i['children']) + factorsArr = [1,1,1,1] + + if 'Factors' in finding: + factors = finding['Factors'] + + factorsArr = map(lambda x: x if ("" + x).isdigit() else 1, [ + factors['Impact']['Business'], + factors['Impact']['Technical'], + factors['Likelihood']['Vulnerability'], + factors['Likelihood']['ThreatAgent'] + ]) + # if it's not a chart, then simply replace. otherwise apply to a chart + if not map(lambda x: etree.ETXPath('.//{%s}chart' % self.ns.c)(x), i['children'])[0]: + self._xml_sdt_replace(i['sdt'], i['children']) + else: + self._xml_apply_chart([[i['struct'], i['sdt'], i['children']]], factorsArr) + #self._xml_sdt_replace(i['sdt'], i['children']) #print '+', finding['Name'], finding['Severity'] else: self._xml_sdt_remove(i['sdt']) #print '-', finding['Name'], finding['Severity'] - # commented out because might have kb value - #else: + else: # # is this finding.[key_not_found]? -> if yes, delete me # #print '?', alias_abs - # alias_search = i['struct'][:] - # alias_search[-1] = alias_search[-1][:-1] - # if not filter(lambda x: x['struct'] == alias_search, aliases): - # self._xml_sdt_remove(i['sdt']) - # #del alias_search + val = finding + for j in i['struct'][1:][:-1] + [i['struct'][-1][:-1]]: + if isinstance(val, list): + if not self._check_list_for_key(val, j): + val = None + break + elif val.has_key(j): + val = val[j] + #print 'o', val + else: + val = None + break + + if val == None or len(val) == 0: + self._xml_sdt_remove(i['sdt']) else: # TODO recent fixes #print 's',i['struct'] @@ -804,6 +840,16 @@ def _xml_apply_findings(self): template_parent.remove(template) del template_parent, template + def _check_list_for_key(self, target_list, key): + for obj in target_list: + if isinstance(obj, list): + return False + + if not obj.has_key(key): + return False + + return True + # Generate Chart def _xml_apply_chart(self, chart_struct, values): #if not self._docx: #... @@ -817,30 +863,99 @@ def _xml_apply_chart(self, chart_struct, values): # #chart_num = etree.ETXPath('//{%s}val/{%s}numRef/{%s}f' % ((self.ns.c,)*3)) (chart)[0] # #print chart_num #chart_rid = etree.ETXPath ('.//{%s}chart' % self.ns.c) (chart_struct[0][2][0])[0].attrib['{%s}id' % self.ns.r] - chart_rid = \ + #chart_struct = copy.deepcopy(chart_structOrg) + # structOrgParent = chart_structOrg.getparent() + # structOrgParent.append(chart_struct) + chart = \ reduce(lambda x, y: x + y, - map(lambda x: etree.ETXPath('.//{%s}chart' % self.ns.c)(x), chart_struct[0][2]))[ - 0].attrib['{%s}id' % self.ns.r] - #print chart_rid - chart_rel_target = filter(lambda x: x['Id'] == chart_rid and x[ + map(lambda x: etree.ETXPath('.//{%s}chart' % self.ns.c)(x), chart_struct[0][2]))[0] + chart_rid = chart.attrib['{%s}id' % self.ns.r] + + # iterator used for identifying copies of charts + self.chartIterator += 1 + chartNewId = chart_rid + 'copy' + str(self.chartIterator) + + chart.attrib['{%s}id' % self.ns.r] = chartNewId + + # charts/chart.xml + chart_rel = filter(lambda x: x.attrib['Id'] == chart_rid and x.attrib[ 'Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart', - map(lambda x: x.attrib, - etree.ETXPath('//{%s}Relationship' % self.ns.a)(self._xml)))[0]['Target'] + etree.ETXPath('//{%s}Relationship' % self.ns.a)(self._xml))[0] + + chart_rel_target = chart_rel.attrib['Target'] + chart_rel_targetCopy = 'copy' + str(self.chartIterator) + chart_rel_target + chart_relCopy = copy.deepcopy(chart_rel) + chart_relCopy.attrib['Id'] = chartNewId + chart_relCopy.attrib['Target'] = chart_rel_targetCopy + chart_relParent = chart_rel.getparent() + chart_relParent.append(chart_relCopy) + #print chart_rel_target chart_part = filter(lambda x: x.attrib['{%s}name' % self.ns.pkg] == '/word/%s' % chart_rel_target, etree.ETXPath('/{%s}package/{%s}part' % ((self.ns.pkg,) * 2))(self._xml))[0] - chart = etree.ETXPath('.//{%s}chart' % self.ns.c)(chart_part)[0] + + chart_partCopy = copy.deepcopy(chart_part) + chart_partCopy.attrib['{%s}name' % self.ns.pkg] = '/word/%s' % chart_rel_targetCopy + chart_partParent = chart_part.getparent() + chart_partParent.append(chart_partCopy) + + # i and values[i]: chart_values[i].text = str(values[i]) else: chart_values[i].text = '0' + #self._xml_sdt_replace(chart_struct['sdt'], chart_struct['children']) chart_parent = chart_struct[0][1].getparent() chart_parent.replace(chart_struct[0][1], chart_struct[0][2][0]) + def _set_document_name(self, test_type, test_name): + pkg = filter(lambda x: x.attrib['{%s}name' % self.ns.pkg] == '/docProps/core.xml', + etree.ETXPath('/{%s}package/{%s}part' % ((self.ns.pkg,) * 2))(self._xml))[0] + coreProperties = etree.ETXPath('.//{%s}coreProperties/{%s}title' \ + % ('http://schemas.openxmlformats.org/package/2006/metadata/core-properties', 'http://purl.org/dc/elements/1.1/'))(pkg)[0] + coreProperties.text = '%s - %s' % (test_type, test_name) + def xml_apply_meta(self, vulnparam_highlighting=True, truncation=True, pPr_annotation=True): # merge kb before generate @@ -852,9 +967,10 @@ def xml_apply_meta(self, vulnparam_highlighting=True, truncation=True, pPr_annot self._pPr_annotation = pPr_annotation # change dir (for the purpose of images handling relatively to template path) pwd = os.getcwd() - os.chdir(os.path.dirname(self._template_filename)) + os.chdir(os.path.dirname(os.path.abspath(self._template_filename))) #abspath needed for CLI # apply self._apply_scan() + # find conditional root elements and make them dictionaries cond = dict() for i in filter(lambda x: len(x[0]) == 1 and x[0][-1][-1] == '?', self._struct): @@ -961,6 +1077,12 @@ def dict_count(block, key, fallback=1): sdt = alias.getparent().getparent() children = etree.ETXPath('./{%s}sdtContent' % self.ns.w)(sdt)[0].getchildren() self._xml_sdt_replace(sdt, children) + + if 'Test' in self._meta['Data']: + test_type = self._meta['Data']['Test']['Type'] if 'Type' in self._meta['Data']['Test'] else 'Test' + test_name = self._meta['Data']['Test']['Name'] if 'Name' in self._meta['Data']['Test'] else 'Application' + + self._set_document_name(test_type, test_name) # restore path os.chdir(pwd) @@ -1052,6 +1174,68 @@ def kb_dump_json(self): def kb_dump_yaml(self): return self._dump_yaml(self._kb) + def nmap_load_xml(self, filename): + self._nmap_filenames.append(filename) + return self.nmap_reload() + + def nmap_remove(self): + self._nmap = None + self._nmap_filenames = [] + self._meta['Data']['OpenPorts'] = None + + def nmap_reload(self): + self._nmap = "" + + if not hasattr(self, '_nmap_filenames') or len(self._nmap_filenames) == 0: + return False + + def append_to_nmap(text): + self._nmap += text + "\n" + + for filename in self._nmap_filenames: + with open(filename) as fd: + try: + doc = etree.fromstring(fd.read()) + except etree.XMLSyntaxError: + self.nmap_remove() + #raise Exception('Error while parsing XML!') + return False + + date, params = None, None + + for x in doc.xpath("//nmaprun"): + date = datetime.fromtimestamp(float(x.attrib['start'])) + parameters = ' '.join(x.attrib['args'].split(' ')[1:]) + + for x in doc.xpath("//host"): + for addr in x.xpath("address[@addrtype!='mac']/@addr"): + h_host = "Open ports on host {}:".format(addr) + append_to_nmap(h_host) + append_to_nmap("-" * len(h_host)) + + ports = [] + for open_p in x.xpath("ports/port[state[@state='open']]"): + ports.append("{port}/{proto}".format( + port=open_p.attrib['portid'], + proto=open_p.attrib['protocol'].upper())) + + append_to_nmap("\n".join(ports)) + + append_to_nmap("") + + append_to_nmap("Scan date: " + str(date)) + + append_to_nmap("") + self._nmap = unicode(self._nmap) + self._apply_nmap_scan() + return True + + def _apply_nmap_scan(self): + self._meta['Data']['OpenPorts'] = self._nmap + + def nmap_dump(self): + return self._nmap + def _apply_scan(self): if self.scan: self._meta['Findings'] += self.scan.findings() diff --git a/src/version.py b/src/version.py index ec2d8cf..8f5024c 100755 --- a/src/version.py +++ b/src/version.py @@ -31,9 +31,17 @@ class Version(object): Generate reports based on HP WebInspect, BurpSuite Pro scans, own custom data, knowledge base and Microsoft Office Word templates. ''' - version = '1.0.2' - date = 'Tue Nov 14 12:12:45 2017' + version = '1.0.3' + date = 'Tuesday Jan 12 11:00:00 2021' changelog = ''' + ''' + version + ''' - ''' + date + ''' + - Nmap scan support for multiple XML files - gets enabled if 'OpenPorts' directive is found in the template + - FIX: Fixed deprecated functions e.g. icon/OnFileDrop handling + - Added support for multiple charts in the _xml_apply_chart functions via objects cloning + - Support for page titles in coreProperties - automatically generated if not set with Test.DocumentTitle + - FIX: Template filename is now resolved with abspath fixing the "empty directory" error + - FIX: Empty scan files directive in CLI would give a NoneType error, changed to an empty array + 1.0.2 - Tue Nov 14 12:12:45 2017 - FIX: Handle null value in Burp custom finding description fields diff --git a/src/yamled.py b/src/yamled.py index 383da65..13ee881 100755 --- a/src/yamled.py +++ b/src/yamled.py @@ -190,6 +190,7 @@ def __init__(self, parent=None, title='', content=None, size=(800, 600,), *args, self.icon = wx.EmptyIcon() self.icon.CopyFromBitmap(myBitmap) self.SetIcon(self.icon) + # tree image list if self.T: self.tree_image_list = wx.ImageList(16, 16) @@ -563,7 +564,8 @@ def DeleteNode(self, item): self.DeleteNode(child) pos = self.n.index(item) self.stack_sizer.Hide(self.t[pos]) - self.stack_sizer.Remove(self.t[pos]) + + self.stack_sizer.Remove(self.t[pos]) # todo: compare changelogs of WX library and find out why it's not passing self.t[pos].Destroy() self.tree.Delete(self.n[pos]) del self.n[pos] @@ -798,7 +800,7 @@ def File_Save(self, e): def File_Save_As(self, e): openFileDialog = wx.FileDialog(self, 'Save Yaml As', '', '', 'Yaml files (*.yaml)|*.yaml|All files (*.*)|*.*', - wx.FD_SAVE | wx.wx.FD_OVERWRITE_PROMPT) + wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) if openFileDialog.ShowModal() == wx.ID_CANCEL: return filename = openFileDialog.GetPath()