# report-ng # Copyright (c) 2014-2017 Marcin Woloszyn (@hvqzao) # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from util import yaml_load, UnsortableOrderedDict import util from openxml import Openxml from pseudohtml import InlineHtmlParser, HtmlParser from scan import Scan import mangle from lxml import etree import yaml, json import os import os.path from cgi import escape #import base64 #import docx docx = None class Report(object): class ns: w = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main' c = 'http://schemas.openxmlformats.org/drawingml/2006/chart' r = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' a = 'http://schemas.openxmlformats.org/package/2006/relationships' pkg = 'http://schemas.microsoft.com/office/2006/xmlPackage' severity = UnsortableOrderedDict([ ('Critical', 4), ('High', 3), ('Medium', 2), ('Low', 1), ('Informational', 0), ('Best Practices', -1) ]) _template_filename = None #_docx = None _xml = None _struct = [] _skel = UnsortableOrderedDict() _content_filename = None _content_yaml = None _content = None _meta = None _kb_filename = None _kb_type = None _kb = None _openxml = None _html_parser = None _ihtml_parser = None scan = None template_cleanup_required = None #__vulnparam_highlighting = True #__truncate = True _pPr_annotation = False def __init__(self): self._meta_init() def _cleanup(self): #if self._docx: # del self._docx #self._docx = None if self._xml: del self._xml self._xml = None if self._openxml: del self._openxml self._openxml = None if self._html_parser: del self._html_parser self._html_parser = None if self._ihtml_parser: del self._ihtml_parser self._ihtml_parser = None if self._struct: del self._struct self._struct = [] if self._skel: del self._skel self._skel = UnsortableOrderedDict() if self._content: del self._content self._content = None if self._meta: del self._meta self._meta_init() if self._kb: del self._kb self._kb = None @staticmethod def _dump_json(target): return json.dumps(target, indent=2, ensure_ascii=False) #.decode('utf-8') def _dump_yaml(self, target): return yaml.dump(target, default_flow_style=False, allow_unicode=True).decode('utf-8') def _reserved(self, value): if value[-1][-1] in ['?', '!']: return True severity_keys = map(lambda x: self._severity_tag(x), self.severity.keys()) if value == ['Findings', 'Chart']: return True if value[0] == 'Findings' and value[1] in severity_keys: return True if value[0] == 'Summary' and value[1] in severity_keys: return True if value == ['Finding']: return True #severity_keys_q = map(lambda x: self._severity_tag(x)+'?', self.severity.keys()) #if value[0] == 'Finding' and value[1] in severity_keys_q: # return True return False def _xml_val(self, children): return ''.join(map(lambda x: x.text, etree.ETXPath('.//{%s}t' % self.ns.w)(children[0]))) def _template_parse(self, clean=False): summary_fields = [] for alias in etree.ETXPath('//{%s}alias' % self.ns.w)(self._xml): value = alias.attrib['{%s}val' % self.ns.w].split('.') sdt = alias.getparent().getparent() children = etree.ETXPath('./{%s}sdtContent' % self.ns.w)(sdt)[0].getchildren() reserved = self._reserved(value) #print ['-','+'][int(reserved)], value self._struct += [[value, sdt, children]] #print value, reserved if not reserved: skel = self._skel for i in range(len(value)): if value[i] not in skel.keys(): skel[value[i]] = UnsortableOrderedDict() if value == ['Finding'] or i == len(value) - 1 and len(children) == 1 and children[0].tag == '{%s}tr' % self.ns.w: skel[value[i]] = [UnsortableOrderedDict()] skel = skel[value[i]] if isinstance(skel, list): skel = skel[0] del skel else: if len(value) == 3 and value[0] == 'Summary'\ and value[1] in map(lambda x: self._severity_tag(x), self.severity.keys()) and value[2] not in ['Finding']: if value[2][-1] != '#' and value[2] not in summary_fields: summary_fields += [value[2]] def leaf(skel): for i in skel.keys(): if isinstance(skel[i], list): leaf(skel[i][0]) if isinstance(skel[i], UnsortableOrderedDict): if not skel[i].keys(): skel[i] = '' else: leaf(skel[i]) leaf(self._skel) if not clean: for i in self._struct: if self._skel.has_key(i[0][0]): x = self._skel[i[0][0]] for j in i[0][1:]: if isinstance(x, list): x = x[0] if j in x: if x[j] == '': x[j] = self._xml_val(i[2]) else: x = x[j] else: continue self._skel['Findings'] = [UnsortableOrderedDict()] for i in ['Name', 'Severity']: i_struct = filter(lambda x: x[0] == ['Finding', i], self._struct) if not clean and len(i_struct): self._skel['Findings'][0][i] = self._xml_val(i_struct[0][2]) else: self._skel['Findings'][0][i] = '' del i_struct if i in self._skel['Finding']: del self._skel['Finding'][i] if summary_fields: self._skel['Findings'][0]['Summary'] = UnsortableOrderedDict() for i in summary_fields: if not clean: self._skel['Findings'][0]['Summary'][i] = self._xml_val( filter(lambda x: len(x[0]) > 2 and x[0][0] == 'Summary' and x[0][2] == i, self._struct)[0][2]) else: self._skel['Findings'][0]['Summary'][i] = '' for i in self._skel['Finding'].keys(): 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) def template_dump_yaml(self): return self._dump_yaml(self._skel) def template_dump_struct(self): import pprint pp = pprint.PrettyPrinter(indent=2) #pp.pprint(self._struct) #pp.pprint(map(lambda x: [x[0], map(lambda y: y.tag[y.tag.index('}') + 1:], x[2])], self._struct)) return pp.pformat(map(lambda x: [x[0], map(lambda y: y.tag[y.tag.index('}') + 1:], x[2])], self._struct)) def template_reload(self, clean=False): self._cleanup() self._xml = etree.parse(self._template_filename) self._openxml = Openxml(self._xml) self._html_parser = etree.XMLParser(target=HtmlParser(self._openxml), resolve_entities=False) self._ihtml_parser = etree.XMLParser(target=InlineHtmlParser(self._openxml), resolve_entities=False) self._template_parse(clean=clean) self._content_parse(self._skel) # TODO added - test if ok self.template_cleanup_required = False return self def template_load_xml(self, filename, clean=False): self._template_filename = filename return self.template_reload(clean=clean) #def template_load_docx(self, filename, clean=False): # self._docx = docx.Document(docx=filename) # self._xml = self._docx._document_part._element # self._template_parse(clean=clean) # return self def save_report_xml(self, filename): if self._xml: self._xml.write(filename, xml_declaration=True, encoding='UTF-8') #if not self._docx: # self._xml.write (filename, xml_declaration=True, encoding='UTF-8') #else: # raise Exception ('docx templates must be saved as docx!') else: raise Exception('no template loaded!') def _meta_init(self): self._meta = UnsortableOrderedDict([('Findings', []), ('KB', []), ('Data', UnsortableOrderedDict())]) def _content_parse(self, meta): self._content = meta.copy() self._meta_init() #self._meta['Findings'] = meta['Findings'] #self._meta['Meta'] = UnsortableOrderedDict() #self._meta['Meta']['Findings'] = UnsortableOrderedDict() #for i in self.severity.keys(): # self._meta['Meta']['Findings'][i] = len (filter (lambda x: x['Severity'] == i, self._meta['Findings'])) #del meta['Findings'] self._meta['Data'] = meta.copy() #self._meta['Findings'] = self._meta['Data']['Findings'][:] if 'Findings' in self._meta['Data']: self._meta['Findings'] = filter(lambda x: x['Severity'] in self.severity.keys(), self._meta['Data']['Findings']) del self._meta['Data']['Findings'] if self._kb: self._kb_meta_update() return self def content_reload(self): if self._content_yaml: self._content_parse(yaml_load(open(self._content_filename).read(), yaml.SafeLoader, UnsortableOrderedDict)) else: self._content_parse(json.loads(open(self._content_filename).read().decode('utf-8-sig'), object_pairs_hook=UnsortableOrderedDict)) return self def content_refresh(self): self._content_parse(self._content) def content_load_yaml(self, filename): self._content_filename = filename self._content_yaml = True return self.content_reload() def content_load_json(self, filename): self._content_filename = filename self._content_yaml = False return self.content_reload() def content_dump_json(self): return self._dump_json(self._content) def content_dump_yaml(self): return self._dump_yaml(self._content) def meta_dump_json(self): return self._dump_json(self._meta) def meta_dump_yaml(self): return self._dump_yaml(self._meta) @staticmethod def _v(struct, path): for i in path: struct = struct[i] return struct @staticmethod def _p(struct, path): for i in path[:-1]: if i not in struct: return None struct = struct[i] return struct def _xml_block_aliases(self, block): aliases = map(lambda x: UnsortableOrderedDict( [('struct', x.attrib['{%s}val' % self.ns.w].split('.')), ('sdt', x.getparent().getparent())]), etree.ETXPath('.//{%s}alias' % self.ns.w)(block)) for i in range(len(aliases)): aliases[i]['children'] = etree.ETXPath('./{%s}sdtContent' % self.ns.w)(aliases[i]['sdt'])[0].getchildren() return aliases #def _xml_sdt_single (self, value, sdt, children): # if children[0].tag in ['{%s}p' % self.ns.w, '{%s}r' % self.ns.w, '{%s}tc' % self.ns.w]: # tag = etree.ETXPath ('.//{%s}t' % self.ns.w) (children[0]) # tag[0].text = unicode(value) # parent = sdt.getparent() # parent.replace(sdt, children[0]) # del tag @staticmethod def _is_html(value): preamble = '<html>' try: return unicode(value).strip().lower()[:len(preamble)] == preamble except: return str(value).strip().lower()[:len(preamble)] == preamble @staticmethod def _is_ihtml(value): preamble = '<ihtml>' try: return unicode(value).strip().lower()[:len(preamble)] == preamble except: return str(value).strip().lower()[:len(preamble)] == preamble def _xml_sdt_single(self, value, sdt, children, struct=None): #print children #if struct: # print struct p_r = filter(lambda x: x.tag in ['{%s}p' % self.ns.w, '{%s}r' % self.ns.w], children) tc = filter(lambda x: x.tag in ['{%s}tc' % self.ns.w], children) p_r_tc = p_r + tc if len(p_r_tc): #print '-' ''' if self._is_html(value): self._openxml.set_sdt_cursor(cursor=sdt) self._openxml.parse(value, self._html_parser) self._openxml.remove_sdt_cursor() elif self._is_ihtml(value): self._openxml.set_sdt_cursor(cursor=sdt) self._openxml.parse(value, self._ihtml_parser) self._openxml.remove_sdt_cursor() ''' if not util.binary(value) and (self._is_html(value) or self._is_ihtml(value)): block = etree.Element('Summary') for i in children: block.append(etree.fromstring(etree.tostring(i))) if self._pPr_annotation == True: pPr = None if len(tc): _tc = filter(lambda x: x.tag in ['{%s}tc' % self.ns.w], block.getchildren()) _p_r = filter(lambda x: x.tag in ['{%s}p' % self.ns.w, '{%s}r' % self.ns.w], _tc[0]) cursor = _p_r[0] else: _p_r = filter(lambda x: x.tag in ['{%s}p' % self.ns.w, '{%s}r' % self.ns.w], block.getchildren()) if self._pPr_annotation == True: # try to get the formatting (pPr) _p = filter(lambda x: x.tag in ['{%s}p' % self.ns.w], block.getchildren()) if len(_p) > 0: pPrs = etree.ETXPath('./{%s}pPr' % self.ns.w)(_p[0]) if len(pPrs) > 0: pPr = pPrs[0] #print etree.tostring(pPr) del _p cursor = _p_r[0] self._openxml.set_sdt_cursor(cursor=cursor) self._openxml.parse(value, [self._html_parser, self._ihtml_parser][self._is_ihtml(value)]) self._openxml.remove_sdt_cursor() parent = sdt.getparent() for i in block.getchildren(): if self._pPr_annotation == True and pPr != None and i.tag in ['{%s}p' % self.ns.w] and len(filter(lambda x: x.tag in ['{%s}pPr' % self.ns.w], i.getchildren())) == 0: i.insert(0, etree.fromstring(etree.tostring(pPr))) parent.insert(parent.index(sdt), i) parent.remove(sdt) else: if util.binary(value): value = util.binary_safe(value) values = unicode(value).split('\n') build = [] for v in values: block = etree.fromstring(etree.tostring(p_r_tc[0])) tags = etree.ETXPath('.//{%s}t' % self.ns.w)(block) if len(tags) > 0: tags[0].text = util.xml_valid_unicode(v) for tag in tags[1:]: tag_parent = tag.getparent() tag_parent.remove(tag) del tag_parent build += [block] del tags, block parent = sdt.getparent() for i in build: parent.insert(parent.index(sdt), i) parent.remove(sdt) del build return True else: return False @staticmethod def surround(text, search, tag, inline=True): text = escape(text) start = text.find('&'+search+'=') if start != -1: start += 5 else: start = text.find(search+'=') if start == -1: return text else: walk = text[start+len(search)+1:] end = walk.find('&') if end == -1: return '<'+['','i'][inline]+'html>'+text[:start]+'<'+tag+'>'+text[start:]+'</'+tag+'>'+'</'+['','i'][inline]+'html>' else: return '<'+['','i'][inline]+'html>'+text[:start]+'<'+tag+'>'+text[start:start+len(search)+1+end]+'</'+tag+'>'+text[start+len(search)+1+end:]+'</'+['','i'][inline]+'html>' return text def _xml_apply_data(self, struct, value, sdt, children): #print '*',struct, value if not children: return if not self._xml_sdt_single(value, sdt, children, struct=struct) and children[0].tag in ['{%s}tr' % self.ns.w]: #print '+',struct build = [] for row in value: #print type(row), row if isinstance(row, str) or isinstance(row, unicode): continue block = etree.fromstring(etree.tostring(children[0])) aliases = self._xml_block_aliases(block) for col in row: #print 'x', col alias_match_q = filter(lambda x: x['struct'] == struct + [str(col)+'?'], aliases) if alias_match_q: #print 'q' if row[col]: self._xml_sdt_replace(alias_match_q[0]['sdt'], alias_match_q[0]['children']) else: self._xml_sdt_remove(alias_match_q[0]['sdt']) del alias_match_q alias_match = filter(lambda x: x['struct'] == struct + [str(col)], aliases) if not alias_match: continue alias = alias_match[0] tags = etree.ETXPath('.//{%s}t' % self.ns.w)(alias['children'][0]) if len(tags) > 0: val = row[col] #if struct == ['Finding', 'Occurrences'] and 'Method' in row and 'VulnParam' in row and row['VulnParam']: if struct == ['Finding', 'Occurrences'] and 'Method' in row: if col == 'Post' and '\n' in val and not self._is_html(val) and not self._is_ihtml(val): # highlighting could be added here as well I suppose val = '<html>'+val.replace('\n','<br/>\n')+'</html>' else: if col == 'Post' and row['Method'] == 'POST': if self.__truncate: val = mangle.http_param_truncate(val) if col == 'Location' and row['Method'] == 'GET' or col == 'Post' and row['Method'] == 'POST': if self.__vulnparam_highlighting and 'VulnParam' in row and row['VulnParam']: val = self.surround(val,row['VulnParam'],'red') if self._is_html(val): self._openxml.set_sdt_cursor(cursor=tags[0]) self._openxml.parse(val, self._html_parser) self._openxml.remove_sdt_cursor() elif self._is_ihtml(val): self._openxml.set_sdt_cursor(cursor=tags[0]) self._openxml.parse(val, self._ihtml_parser) self._openxml.remove_sdt_cursor() elif isinstance(val, list): pass # TODO #print struct+[str(col)], val self._xml_apply_data(struct+[str(col)], val, alias['sdt'], alias['children']) elif val == None: tags[0].text = '' else: #tags[0].text = unicode(val) tags[0].text = util.xml_valid_unicode(val) for tag in tags[1:]: tag_parent = tag.getparent() tag_parent.remove(tag) del tag_parent parent = alias['sdt'].getparent() if parent != None: parent.replace(alias['sdt'], alias['children'][0]) del alias_match, alias build += [block] del aliases, block parent = sdt.getparent() for i in build: parent.insert(parent.index(sdt), i) parent.remove(sdt) del build #print #struct_q = struct[:] #struct_q[-1] += '?' #if self #print struct def _xml_sdt_replace (self, sdt, children): parent = sdt.getparent() if parent != None: for i in children: parent.insert(parent.index(sdt), i) parent.remove(sdt) def _xml_sdt_remove (self, sdt): parent = sdt.getparent() if parent != None: parent.remove(sdt) @staticmethod def _severity_tag(severity): if severity == 'Best Practices': return 'BestPractices' else: return severity @staticmethod def _kb_val(finding_val, kb_val): #if not kb_val: # return finding_val #if not finding_val: # return kb_val #return finding_val+'\n'+kb_val if finding_val: return finding_val elif kb_val: return kb_val else: return '' ''' get knowledgebase item for given finding [name, severity] ''' def _kb_match(self, name, severity, knowledgebase): kb = None if self._kb: kb_match = filter(lambda x: x['Name'] == name and x['Severity'] == severity, knowledgebase) if kb_match: kb = kb_match[0] del kb_match if kb == None: # find matching alias, if such exist #kb_match_aliases = filter(lambda x: 'Aliases' in x and name in x['Aliases'].split('\n') and x['Severity'] == severity, knowledgebase) kb_match_aliases = filter(lambda x: 'Aliases' in x and unicode(name).replace(u'\u200b','').lower() in x['Aliases'].lower().split('\n'), knowledgebase) #for i in map(lambda x: x['Aliases'].split('\n'), knowledgebase): # print [name], unicode(name).replace(u'\u200b','') in i, i #print name, kb_match_aliases != [] if kb_match_aliases: kb = kb_match_aliases[0] del kb_match_aliases return kb def remove_kb(self): self._kb_filename = None self._kb_type = None if self._kb: del self._kb self._kb = None self._meta['KB'] = [] ''' replaces kb updates during report generation ''' def merge_kb(self): if self._kb == None or self._kb.keys() == []: return #self._content #self._kb #print self._dump_yaml(self._kb) def submerge(block, knowledge, path): for node in block: if path == [] and node in ['Name', 'Severity']: continue if isinstance(block[node], dict) and knowledge.has_key(node): #print len(path)*2*' ', node + ':' submerge(block[node], knowledge[node], path + [node]) continue #print len(path)*2*' ', node + ':', type(finding[node]) if not filter(lambda x: isinstance(block[node], x), [list, dict]): if block[node] != None and len(block[node].strip()) == 0 and knowledge.has_key(node): block[node] = knowledge[node] knowledgebase = self._kb[self._kb.keys()[0]][:] #print self._dump_yaml(kb) if self._content != None and isinstance(self._content, dict) and self._content.has_key('Findings'): for finding in self._content['Findings']: kb = self._kb_match(finding['Name'], finding['Severity'], knowledgebase) if kb != None: submerge(finding, kb, []) #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) #print map(lambda x: x['Name'], self._meta['Findings']) findings = self._meta['Findings'] for severity in self.severity.keys(): severity_tag = self._severity_tag(severity) severity_findings = filter(lambda x: x['Severity'] in severity, findings) findings_placeholder_row = filter(lambda x: x[0] == ['Findings', severity_tag], self._struct) if len(findings_placeholder_row): findings_placeholder = findings_placeholder_row[0][1] findings_placeholder_parent = findings_placeholder.getparent() summary_struct = filter(lambda x: x[0] == ['Summary', severity_tag], self._struct) #print summary_struct if summary_struct: summary_placeholder = summary_struct[0][1] summary_placeholder_parent = summary_placeholder.getparent() #print summary_placeholder #print summary_placeholder_parent for finding in severity_findings: #print '[*]',finding['Name'] #print finding kb = self._kb_match(finding['Name'], finding['Severity'], self._meta['KB']) #print '?',kb != None # Build Finding xml block block = etree.Element('Finding') for i in finding_struct[0][2]: block.append(etree.fromstring(etree.tostring(i))) aliases = self._xml_block_aliases(block) aliases_abs = map(lambda x: '.'.join(x['struct']), aliases) for i in aliases: alias_abs = '.'.join(i['struct']) #print ' ',alias_abs # handle aliases ending with ! (acts as "if not", eg. Finding.Node not set or set to '') #if alias_abs[-1] == '!' and len(i['struct'][1:]) == 1: # needle = i['struct'][-1][:-1] # if not finding.has_key(needle) or not len(finding[needle]): # self._xml_sdt_replace(i['sdt'], i['children']) # else: # self._xml_sdt_remove(i['sdt']) # del needle # continue # instead of above, should handle Finding.root and Finding.[...].child elements if alias_abs[-1] == '!' and len(i['struct'][1:]) >= 1: #print i['struct'] val = finding for j in i['struct'][1:][:-1] + [i['struct'][-1][:-1]]: if val.has_key(j): val = val[j] #print 'o', val else: val = None break if val != None and len(val) > 0: self._xml_sdt_remove(i['sdt']) else: self._xml_sdt_replace(i['sdt'], i['children']) continue if alias_abs[-1] == '?': # 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']) #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: # # 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 else: # TODO recent fixes #print 's',i['struct'] #if self._p(finding, i['struct'][1:]) == None: # print 'x',None # continue if self._p(finding, i['struct'][1:]) != None and not isinstance(self._p(finding, i['struct'][1:]), list) and i['struct'][1:][-1] in self._p(finding, i['struct'][1:]): #if self._p (finding, i['struct'][1:]).has_key(i['struct'][1:][-1]): finding_val = self._v(finding, i['struct'][1:]) else: finding_val = '' kb_val = None #print ' ', i['struct'], kb != None if kb: if i['struct'] not in [['Finding', 'Name'], ['Finding', 'Severity']]: kb_p = self._p(kb, i['struct'][1:]) if kb_p and kb_p.has_key(i['struct'][1:][-1]): kb_val = self._v(kb, i['struct'][1:]) del kb_p #self._xml_sdt_single (self._kb_val (finding_val, kb_val), i['sdt'], i['children']) ultimate_val = self._kb_val(finding_val, kb_val) #print '+', alias_abs #print '=',ultimate_val #print i['struct'], ultimate_val, i['sdt'] self._xml_apply_data(i['struct'], ultimate_val, i['sdt'], i['children']) #print finding['Name'], i['struct'] alias_search = i['struct'][:] alias_search[-1] += '?' alias_search_matched = filter(lambda x: x['struct'] == alias_search, aliases) if alias_search_matched: # and not isinstance(ultimate_val, list): #print alias_search_matched[0]['struct'] #print ' ',alias_search, alias_search_matched, bool(ultimate_val) # value present? -> leave contents, otherwise delete me if ultimate_val: self._xml_sdt_replace(alias_search_matched[0]['sdt'], alias_search_matched[0]['children']) else: self._xml_sdt_remove(alias_search_matched[0]['sdt']) del alias_search_matched del kb_val, finding_val, ultimate_val del alias_abs # Insert Finding if len(findings_placeholder_row) > 0: for i in block.getchildren(): findings_placeholder_parent.insert(findings_placeholder_parent.index(findings_placeholder), i) del block, aliases, aliases_abs # Summary row if summary_struct: # Build a summary row xml block block = etree.Element('Summary') for i in summary_struct[0][2]: block.append(etree.fromstring(etree.tostring(i))) aliases = self._xml_block_aliases(block) for i in aliases: #print '+',i['struct'] if i['struct'] == ['Summary', severity_tag, 'Finding']: self._xml_sdt_single(finding['Name'], i['sdt'], i['children']) elif i['struct'][-1][-1] == '#': i_hash = i['struct'][:] i_hash[-1] = i_hash[-1][:-1] if i_hash[2:][0] in finding: i_hash_count = len(self._v(finding, i_hash[2:])) or 1 else: i_hash_count = 1 self._xml_sdt_single(i_hash_count, i['sdt'], i['children']) del i_hash else: #print finding if 'Summary' in finding and i['struct'][2] in finding['Summary']: finding_val = finding['Summary'][i['struct'][2]] elif finding.has_key(i['struct'][2]): finding_val = finding[i['struct'][2]] else: finding_val = '' kb_val = None if kb: if 'Summary' in kb and i['struct'][2] in kb['Summary']: kb_val = kb['Summary'][i['struct'][2]] self._xml_sdt_single(self._kb_val(finding_val, kb_val), i['sdt'], i['children']) del kb_val, finding_val for i in block.getchildren(): summary_placeholder_parent.insert(summary_placeholder_parent.index(summary_placeholder), i) del block, aliases del kb if findings_placeholder_row: if summary_struct: summary_placeholder_parent.remove(summary_placeholder) if len(severity_findings) > 0: findings_placeholder_parent.remove(findings_placeholder) else: if len(findings_placeholder_row): if self._xml_val(findings_placeholder_row[0][2]).strip() != '': self._xml_sdt_replace(findings_placeholder, findings_placeholder_row[0][2]) else: self._xml_sdt_remove(findings_placeholder) del findings_placeholder, findings_placeholder_parent del findings_placeholder_row if summary_struct: del summary_placeholder, summary_placeholder_parent del severity_findings, summary_struct # Remove Finding template template = finding_struct[0][1] template_parent = template.getparent() template_parent.remove(template) del template_parent, template def _xml_apply_chart(self, chart_struct, values): #if not self._docx: #... #else: # #>>> a = docx.Document(docx='d3.docx') # #>>> p=a._package # chart_rid = 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] # chart_part = filter (lambda x: chart_rid in x and x[chart_rid].reltype == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart', map (lambda x: x.rels, self._docx._package.parts))[0][chart_rid].target_part.package#._element #.package.main_document._element # print chart_part # #chart = etree.ETXPath ('.//{%s}chart' % self.ns.c) (chart_part)[0] # #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 = \ 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[ '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'] #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] #print etree.ETXPath('.//{%s}t' % 'http://schemas.openxmlformats.org/drawingml/2006/main')(chart)[0].text chart_num = etree.ETXPath('.//{%s}val/{%s}numRef/{%s}f' % ((self.ns.c,) * 3))(chart)[0] chart_values = etree.ETXPath('.//{%s}v' % self.ns.c)(chart_num.getparent().getparent()) for i in range(len(chart_values)): if values[i]: chart_values[i].text = str(values[i]) else: chart_values[i].text = '0' chart_parent = chart_struct[0][1].getparent() chart_parent.replace(chart_struct[0][1], chart_struct[0][2][0]) def xml_apply_meta(self, vulnparam_highlighting=True, truncation=True, pPr_annotation=True): # merge kb before generate self.merge_kb() self.template_cleanup_required = True self.__vulnparam_highlighting = vulnparam_highlighting self.__truncate = truncation 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)) # 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): cond[i[0][-1][:-1]] = [] #print cond # find if-not-exists notx = dict() for i in filter(lambda x: len(x[0]) == 1 and x[0][-1][-1] == '!', self._struct): notx[i[0][-1][:-1]] = [] # process Data.* #for i in self._struct: def dict_count(block, key, fallback=1): if key in block: return len(block[key]) or fallback else: return fallback for i in filter(lambda x: x[0][-1][-1] not in ['?', '!'], self._struct): p = self._p(self._meta['Data'], i[0]) #if p is not None and i[0] != ['Finding']: # match_q = i[0][:] # match_q[-1] += '?' # print '?',i[0], p, len(filter(lambda x: x[0] == match_q, self._struct)) if p is not None and not isinstance(p, list) and i[0][-1] in p: #print '+',i[0] val = self._v(self._meta['Data'], i[0]) #print i[0], bool(val) if i[0][0] in cond.keys() and bool(val): cond[i[0][0]] += [i[0]] if i[0][0] in notx.keys() and bool(val): notx[i[0][0]] += [i[0]] if val == None: val = '' self._xml_apply_data(i[0], val, i[1], i[2]) # handle conditional sub elements i_search = i[0][:] i_search[-1] = i_search[-1]+'?' i_search_match = filter(lambda x: i_search == x[0], self._struct) for match in i_search_match: #print i_search, i_search_match if val: self._xml_sdt_replace(match[1], match[2]) else: self._xml_sdt_remove(match[1]) del i_search_match # handle if-not-exists sub elements x_search = i[0][:] x_search[-1] = x_search[-1]+'!' x_search_match = filter(lambda x: x_search == x[0], self._struct) for match in x_search_match: #print x_search, x_search_match if val: self._xml_sdt_remove(match[1]) else: self._xml_sdt_replace(match[1], match[2]) del x_search_match # if conditional root element does not have any members with values, remove them #print cond for i in cond.keys(): i_struct = filter(lambda x: x[0] == [i+'?'], self._struct) if len(cond[i]): self._xml_sdt_replace(i_struct[0][1], i_struct[0][2]) else: self._xml_sdt_remove(i_struct[0][1]) del i_struct # if if-not-exists root element does not have any members with values, remove them #print notx for i in notx.keys(): x_struct = filter(lambda x: x[0] == [i+'!'], self._struct) if len(cond[i]): self._xml_sdt_remove(x_struct[0][1]) else: self._xml_sdt_replace(x_struct[0][1], x_struct[0][2]) del x_struct # Findings self._xml_apply_findings() # Findings.Chart chart_struct = filter(lambda x: x[0] == ['Findings', 'Chart'], self._struct) if chart_struct: findings_by_severity_map = map(lambda x: x['Severity'], self._meta['Findings']) findings_by_severity = map(lambda x: len(filter(lambda y: x == y, findings_by_severity_map)), self.severity.keys()) self._xml_apply_chart(chart_struct, findings_by_severity) del chart_struct # Findings.VolumeChart chart_struct = filter(lambda x: x[0] == ['Findings', 'VolumeChart'], self._struct) if chart_struct: findings_by_volume_map = map(lambda x: [x['Severity'], dict_count(x, 'Occurrences', fallback=1)], self._meta['Findings']) findings_by_volume = map(lambda z: reduce(lambda x,y: x+y, map(lambda x: x[1], filter(lambda x: x[0] == z, findings_by_volume_map))+[0]), self.severity.keys()) self._xml_apply_chart(chart_struct, findings_by_volume) del chart_struct # handle conditionals for missing tags definition in content for i in filter(lambda x: len(x[0]) > 1 and x[0][0] not in ['Finding', 'Findings'] and x[0][-1][-1] == '?', self._struct): i_match_search = i[0][:] i_match_search[-1] = i_match_search[-1][:-1] i_match = filter(lambda x: x[0] == i_match_search, self._struct) self._xml_sdt_remove(i[1]) del i_match # sdt cleanup for alias in etree.ETXPath('//{%s}alias' % self.ns.w)(self._xml): value = alias.attrib['{%s}val' % self.ns.w].split('.') sdt = alias.getparent().getparent() children = etree.ETXPath('./{%s}sdtContent' % self.ns.w)(sdt)[0].getchildren() self._xml_sdt_replace(sdt, children) # restore path os.chdir(pwd) self.remove_kb() def _kb_meta_update(self): if 'KB' in self._kb: self._meta['KB'] = self._kb['KB'][:] elif 'Findings' in self._kb: self._meta['KB'] = self._kb['Findings'][:] else: raise Exception('Knowledge base file must be have KB or Findings section!') def _kb_load_csv(self, filename): import csv def transcode(file, decode='cp1250', encode='utf-8'): for line in file: yield line.decode(decode).encode(encode) data = [] with open(filename, 'rb') as csvfile: # CSV Reader - make it compatible with Polish regional settings. To adjust English settings: # open Control Panel, Region and Language, Additional settings... List separator: ";" for row in csv.reader(transcode(csvfile), delimiter=';', quotechar='"'): data += [row] columns = data[0] rows = data[1:] results = [] for row in rows: item = UnsortableOrderedDict() for col in range(len(columns)): value = unicode(row[col].decode('utf-8')) colname = columns[col] if colname == 'Vulnerability Name': colname = 'Name' if colname == 'Vulnerability Aliases': colname = 'Aliases' if colname in ['Modified By', 'Item Type', 'Path']: continue #if colname == 'Name': # print '[+]', value colname = unicode(colname.decode('utf-8')).replace(' ','').replace('\'','') value = filter(lambda x: x not in [u'\xa0', u'\x3f'], value) if colname == 'Aliases': #value = '\n'.join(filter(lambda x: len(x) > 0, map(lambda x: ''.join(filter(lambda y: y not in [u'\xa0', u'\x3f'], list(x))), value.split('\n')))) value = '\n'.join(filter(lambda x: len(x.strip()) > 0, value.split('\n'))) colname_tree = colname.split('.') if len(colname_tree) > 1: node = item for colname_node in colname_tree[:-1]: if colname_node not in item or not isinstance(item, UnsortableOrderedDict): item[colname_node] = UnsortableOrderedDict() node = item[colname_node] node[colname_tree[-1]] = value else: item[colname] = value results += [item] return UnsortableOrderedDict([('KB',results,)]) def kb_reload(self): if self._kb_type == 'yaml': self._kb = yaml_load(open(self._kb_filename).read(), yaml.SafeLoader, UnsortableOrderedDict) elif self._kb_type == 'json': self._kb = json.loads(open(self._kb_filename).read().decode('utf-8-sig'), object_pairs_hook=UnsortableOrderedDict) else: self._kb = self._kb_load_csv(self._kb_filename) self._kb_meta_update() def kb_load_yaml(self, filename): self._kb_filename = filename self._kb_type = 'yaml' self.kb_reload() def kb_load_json(self, filename): self._kb_filename = filename self._kb_type = 'json' self.kb_reload() def kb_load_csv(self, filename): self._kb_filename = filename self._kb_type = 'csv' self.kb_reload() def kb_dump_json(self): return self._dump_json(self._kb) def kb_dump_yaml(self): return self._dump_yaml(self._kb) def _apply_scan(self): if self.scan: self._meta['Findings'] += self.scan.findings() self._meta['Findings'].sort(key=lambda x: x['Severity'], reverse=True) def merge_scan(self, scan): if 'Findings' not in self._content: # create empty self._content['Findings'] list self._content['Findings'] = [] # if self._content len == 0 if len(self._content['Findings']) == 0: # just copy scan['Findings'] #self._content['Findings'] = scan._scan['Findings'][:] self._content['Findings'] = scan['Findings'][:] else: # for each finding in scan['Findings'] #for finding in scan._scan['Findings']: for finding in scan['Findings']: # if name and severity matches: finding_match = filter(lambda x: x['Name'] == finding['Name'] and x['Severity'] == finding['Severity'], self._content['Findings']) if finding_match: # add occurrences (if not duplicated) for occurrence in finding['Occurrences']: occurrence_match = filter(lambda x: x == occurrence, finding_match[0]['Occurrences']) if not occurrence_match: finding_match[0]['Occurrences'] += [occurrence] else: # otherwise copy whole finding self._content['Findings'] += [finding] del finding_match # filter out self._content['Findings'] where severity == '' self._content['Findings'] = filter(lambda x: x['Severity'] != '' , self._content['Findings']) if len(self._content['Findings']) == 0: del self._content['Findings'] if __name__ == '__main__': pass Scan('../workbench/burp-1/burp.xml') ''' # Standard, demo operation (example with two scans and knowledge base) report = Report() report.template_load_xml('../examples/example-2A-scan-report-template.xml', clean=True) report.content_load_yaml('../examples/example-2B-content.yaml') #report.scan = Scan('../examples/example-2-scan-export-Burp.xml') scan = Scan('../examples/example-2C-scan-export-Burp.xml') report.merge_scan(scan.modify()) report.content_refresh() scan2 = Scan('../examples/example-2C-scan-export-WebInspect.xml') report.merge_scan(scan2.modify()) report.content_refresh() #report.kb_load_yaml('../examples/example-2D-kb.yaml') report.kb_load_csv('../examples/example-2D-kb.csv') report.merge_kb() report.xml_apply_meta() report.save_report_xml('../examples/\!.xml') ''' ''' # html-formatting-1 report = Report() report.template_load_xml('../testcase/html-formatting-1/1-template.xml', clean=True) report.content_load_yaml('../testcase/html-formatting-1/2-content.yaml') report.xml_apply_meta() report.save_report_xml('../testcase/html-formatting-1/!.xml') ''' ''' # conditional "if not" for non-finding root nodes report = Report() report.template_load_xml('../testcase/non-finding-if-not-1/1-template.xml', clean=True) report.content_load_yaml('../testcase/non-finding-if-not-1/2-content.yaml') report.xml_apply_meta() report.save_report_xml('../testcase/non-finding-if-not-1/!.xml') ''' ''' report = Report() report.template_load_xml('../workbench/no-bestpractices-2/1.xml', clean=True) report.content_load_yaml('../workbench/no-bestpractices-2/2.yaml') report.xml_apply_meta() #report.save_report_xml('../workbench/no-bestpractices-2/!.xml') ''' ''' report = Report() report.template_load_xml('../workbench/merge-kb-fix-1/1.xml', clean=True) report.content_load_yaml('../workbench/merge-kb-fix-1/2.yaml') report.kb_load_csv('../workbench/merge-kb-fix-1/4.csv') report.merge_kb() #print '---' print report.content_dump_yaml() #report.xml_apply_meta() #report.save_report_xml('../workbench/merge-kb-fix-1/!.xml') ''' #report = Report() #report.template_load_xml('../workbench/no-bestpractices-1/3.xml', clean=True) #report.content_load_yaml('../workbench/no-bestpractices-1/3.yaml') #report.xml_apply_meta() #report.save_report_xml('../workbench/no-bestpractices-1/!.xml') ''' # conditional "if not" for Finding child nodes report = Report() report.template_load_xml('../testcase/if-not-2/1-template.xml', clean=True) report.content_load_yaml('../testcase/if-not-2/2-content.yaml') report.xml_apply_meta() report.save_report_xml('../testcase/if-not-2/!.xml') ''' ''' # conditional "if not" for Finding root nodes report = Report() report.template_load_xml('../testcase/if-not-1/1-template.xml', clean=True) report.content_load_yaml('../testcase/if-not-1/2-content.yaml') report.xml_apply_meta() report.save_report_xml('../testcase/if-not-1/!.xml') ''' ''' # conditional "if" report = Report() report.template_load_xml('../testcase/if-1/1-template.xml', clean=True) report.content_load_yaml('../testcase/if-1/2-content.yaml') report.xml_apply_meta() report.save_report_xml('../testcase/if-1/!.xml') ''' ''' report = Report() report.template_load_xml('../workbench/1-template.xml', clean=True) report.content_load_yaml('../workbench/2-content.yaml') #scan = Scan('../workbench/3-scan.yaml') #scan.dump_yaml() #report.merge_scan(scan) report.kb_load_csv('../workbench/4-kb.csv') report.merge_kb() #print '---' print report.content_dump_yaml() # #print report._dump_yaml(report._content) #print report._dump_yaml(report._meta) #print report._dump_yaml(report._kb) #print report._kb['KB'][0] #print report.meta_dump_yaml() #print report.kb_dump_yaml() #report.xml_apply_meta() #report.save_report_xml('../workbench/!.xml') #print report.content_dump_yaml() ''' ''' report = Report() report.template_load_xml('../workbench/pd1/x.xml', clean=True) report.content_load_yaml('../workbench/pd1/x.yaml') report.xml_apply_meta() #report.save_report_xml('../workbench/pd1/!.xml') ''' #report = Report() #report.template_load_xml('../workbench/pd1/x.xml', clean=True) #report.content_load_yaml('../workbench/pd1/y.yaml') #report.xml_apply_meta() #report.save_report_xml('../workbench/pd1/!!.xml') ''' report = Report() #report.kb_load_yaml('../examples/example-2-kb.yaml') #print report._kb #report.kb_load_csv('../workbench/test-KB.csv') report.kb_load_csv('../workbench/KB.csv') #print report._kb['KB'][0] #print report.meta_dump_yaml() print report.kb_dump_yaml() ''' #report = Report() #report.template_load_xml('../workbench/pb1/templ.xml', clean=True) #report.scan = Scan('../workbench/pb1/is.xml') #report.scan.dump_yaml() #report.xml_apply_meta() #report.save_report_xml('../workbench/pb1/!.xml') ''' report = Report() #report.template_load_xml('../workbench/s/PT template v1.4.xml', clean=True) report.template_load_xml('../workbench/k/PT Polish template v1.0.xml', clean=True) #report.content_load_yaml('../workbench/s/x.yaml') report.content_load_yaml('../workbench/k/PT Polish content.yaml') #report.scan = Scan('../workbench/j/...yaml') #report.kb_load_csv('../workbench/j/KB_latest.csv') report.xml_apply_meta() report.save_report_xml('../workbench/k/!.xml') ''' ''' report = Report() report.template_load_xml('../workbench/p/templ.xml', clean=True) report.content_load_yaml ('../workbench/p/templ.yaml') report.scan = Scan('../workbench/p/scan.yaml') report.kb_load_csv('../workbench/p/kb.csv') report.xml_apply_meta() report.save_report_xml('../workbench/p/!.xml') ''' ''' report = Report() report.kb_load_csv('../workbench/KB-1.csv') #print report.kb_dump_yaml() #print filter(lambda x: 'Hidden' in x['Name'], report._kb['KB']) for i in map(lambda x:x['Aliases'], report._kb['KB']): print i.split('\n') print #report.template_load_xml('../workbench/bug-01/template.xml', clean=True) #report.scan = Scan('../workbench/bug-01/burp scan bug.yaml') #report.xml_apply_meta() #report.save_report_xml('../workbench/bug-01/!.xml') ''' ''' report = Report() report.template_load_xml('../workbench/issue/a.xml', clean=True) report.content_load_yaml ('../workbench/issue/a.yaml') report.scan = Scan('../workbench/issue/b.xml') #report.kb_load_csv('../workbench/issue/a.csv') report.xml_apply_meta() #report.save_report_xml('../workbench/issue/!.xml') ''' #report = Report() #report.scan = Scan('../workbench/_prep-joined.yaml') ''' # sample DS report = Report() report.template_load_xml('../workbench/s-template v1.0.xml', clean=True) report.content_load_yaml ('../workbench/s-template v1.0-content.yaml') report.kb_load_csv('../workbench/KB.csv') report.scan = Scan('../workbench/b-webinspect.xml') report.xml_apply_meta() report.save_report_xml('../workbench/output-2.xml') ''' ''' report = Report() #report.template_load_xml('../workbench/example-2-webinspect-report-template.xml', clean=True) #report.template_load_xml('../workbench/example-2-scan-report-template.xml', clean=True) report.template_load_xml('../workbench/pt-template-v0.6-v0.2.xml', clean=False) #print report.template_dump_yaml() #report.content_load_yaml ('../workbench/example-2-content.yaml') #report.content_load_yaml ('../workbench/test-v0.3-content.yaml') report.content_load_yaml ('../workbench/pt-template-v0.6-v0.2-content.yaml') #report.kb_load_yaml('../examples/example-2-kb.yaml') #report.kb_load_csv('../workbench/Knowledge Base.csv') report.kb_load_csv('../workbench/KB.csv') #print report.kb_dump_yaml() #scan = Scan('../workbench/b-webinspect.xml') #scan= Scan('../workbench/b-burp.xml') #print scan.dump_yaml() #report.scan = scan #print report.content_dump_yaml() #print report.scan.dump_yaml() #print report._content #print scan._scan['Findings'] #report.merge_scan(scan) #print report._content #report.scan = Scan('../workbench/b-webinspect.xml') #report.scan = Scan('../workbench/b-webinspect.yaml') #report.scan = Scan('../workbench/b-burp.xml') #report.scan = Scan('../workbench/b-burp.yaml') #report.scan = Scan('../workbench/a-webinspect-http.xml') #report.scan = Scan('../workbench/z-webinspect.xml') #report.scan = Scan('../workbench/c-webinspect.yaml') #print map(lambda x: x['Name'], report.scan._scan['Findings']) #print report.scan.dump_yaml() #report.xml_apply_meta(vulnparam_highlighting=False) report.xml_apply_meta() #print report.meta_dump_yaml() #report.save_report_xml('../workbench/output.xml') #report.save_report_xml('../workbench/output-2.xml') report.save_report_xml('../workbench/test-output.xml') #print 'end.' ''' ''' report = Report() report.template_load_xml ('../workbench/example-1-content-report-template.xml') #report.template_reload(clean=True) #print report.template_dump_struct() #print report.template_dump_yaml() report.content_load_yaml ('../workbench/example-1-content.yaml') report.xml_apply_meta() report.save_report_xml('../workbench/output.xml') #report.template_reload(clean=True) #report.content_load_yaml ('../examples/example-1-content.yaml') #report.xml_apply_meta() #report.save_report_xml('../workbench/output.xml') ''' ''' report = Report() report.scan = Scan('../workbench/b-burp.xml') print report.scan.dump_yaml() ''' ''' report = Report() report.template_load_xml ('../examples/example-1-content-report-template.xml') #print report.template_dump_struct() report.content_load_yaml ('../examples/example-1-content.yaml') report.xml_apply_meta() report.save_report_xml('../workbench/output.xml') ''' ''' report = Report() #report.template_load_xml('d4-example-report-template.xml', clean=True) #report.template_load_xml('d4-example-report-template.xml') #report.template_load_xml('d9-test-template.xml', clean=True) #report.template_load_xml('d9-test-template.xml') #report.template_load_xml('d9-simple-template.xml', clean=True) #report.template_load_xml('d9-simple-template.xml') report.template_load_xml('d9-webinspect-10-template.xml') #print report.template_dump_struct() #report.template_load_docx('d3.docx') #report.template_reload(clean=True) #print report.template_dump_json() #print report.template_dump_yaml() #print '---' ##report.content_load_yaml('d4-example-content.yaml') #report.content_load_yaml('d5-kb-content.yaml') #report.content_load_yaml('d9-test-content.yaml') #report.content_load_yaml('d9-simple-content.yaml') #report.kb_load_yaml('d5-kb.yaml') #print report.meta_dump_yaml() #print report.kb_dump_yaml() #print report.meta_dump_json() #report.scan = Scan('d8-webinspect-scan.xml') report.scan = Scan('d9-webinspect-10-scan.xml') #print report.scan.dump_yaml() #report._apply_scan() #print report.meta_dump_yaml() #report.content_load_json('d3.json') report.xml_apply_meta() report.save_report_xml('done.xml') ''' ''' # test: merge_scan report = Report() report.content_load_yaml('../examples/example-2-content.yaml') scan = Scan('../workbench/test-case-example-2-merge-1.yaml') report.merge_scan(scan) scan = Scan('../workbench/test-case-example-2-merge-2.yaml') report.merge_scan(scan) scan = Scan('../workbench/test-case-example-2-merge-3.yaml') report.merge_scan(scan) scan = Scan('../workbench/test-case-example-2-merge-4.yaml') report.merge_scan(scan) print report.content_dump_yaml() '''