3 # Convert our XML file to a JSON file as accepted by Mitre for CNA purposes
4 # as per https://github.com/CVEProject/automation-working-group/blob/master/cve_json_schema/DRAFT-JSON-file-format-v4.md
6 # ASF httpd and OpenSSL use quite similar files, so this script is designed to work with either
9 from xml.dom import minidom
11 import simplejson as json
14 from optparse import OptionParser
19 from jsonschema import validate
20 from jsonschema import Draft4Validator
23 # Specific project stuff is here
24 import vulnxml2jsonproject as cfg
26 # Location of CVE JSON schema (default, can use local file etc)
27 default_cve_schema = "https://raw.githubusercontent.com/CVEProject/automation-working-group/master/cve_json_schema/CVE_JSON_4.0_min_public.schema"
29 parser = OptionParser()
30 parser.add_option("-s", "--schema", help="location of schema to check (default "+default_cve_schema+")", default=default_cve_schema,dest="schema")
31 parser.add_option("-i", "--input", help="input vulnerability file vulnerabilities.xml", dest="input")
32 parser.add_option("-c", "--cve", help="comma separated list of cve names to generate a json file for (or all)", dest="cves")
33 parser.add_option("-o", "--outputdir", help="output directory for json file (default ./)", default=".", dest="outputdir")
34 (options, args) = parser.parse_args()
37 print "needs input file"
42 response = urllib.urlopen(options.schema)
43 schema_doc = json.loads(response.read())
47 with codecs.open(options.input,"r","utf-8") as vulnfile:
48 vulns = vulnfile.read()
49 dom = minidom.parseString(vulns.encode("utf-8"))
51 for issue in dom.getElementsByTagName('issue'):
52 if not issue.getElementsByTagName('cve'):
54 # ASF httpd has CVE- prefix, but OpenSSL does not, make either work
55 cvename = issue.getElementsByTagName('cve')[0].getAttribute('name').replace('CVE-','')
58 if (options.cves): # If we only want a certain list of CVEs, skip the rest
59 if (not cvename in options.cves):
63 cve['data_type']="CVE"
64 cve['data_format']="MITRE"
65 cve['data_version']="4.0"
66 cve['CVE_data_meta']= { "ID": "CVE-"+cvename, "ASSIGNER": cfg.config['cve_meta_assigner'], "STATE":"PUBLIC" }
67 datepublic = issue.getAttribute("public")
69 cve['CVE_data_meta']['DATE_PUBLIC'] = datepublic[:4]+'-'+datepublic[4:6]+'-'+datepublic[6:8]
70 if issue.getElementsByTagName('title'):
71 cve['CVE_data_meta']['TITLE'] = issue.getElementsByTagName('title')[0].childNodes[0].nodeValue.strip()
73 for d in issue.getElementsByTagName('description')[0].childNodes:
74 # if d.nodeType == d.ELEMENT_NODE:
77 desc += re.sub('<[^<]+?>', '', d.toxml().strip())
78 desc = HTMLParser.HTMLParser().unescape(desc)
79 problemtype = "(undefined)"
80 if issue.getElementsByTagName('problemtype'):
81 problemtype = issue.getElementsByTagName('problemtype')[0].childNodes[0].nodeValue.strip()
82 cve['problemtype'] = { "problemtype_data": [ { "description" : [ { "lang":"eng", "value": problemtype} ] } ] }
83 impact = issue.getElementsByTagName('impact') # openssl does it like this
85 cve['impact'] = [ { "lang":"eng", "value":impact[0].getAttribute('severity'), "url":cfg.config['security_policy_url']+impact[0].getAttribute('severity') } ]
86 impact = issue.getElementsByTagName('severity') # httpd does it like this
88 cve['impact'] = [ { "lang":"eng", "value":impact[0].childNodes[0].nodeValue, "url":cfg.config['security_policy_url']+impact[0].childNodes[0].nodeValue } ]
90 # Create the list of credits
93 for reported in issue.getElementsByTagName('reported'): # openssl style credits
94 credit.append( { "lang":"eng", "value":re.sub('[\n ]+',' ', reported.getAttribute("source"))} )
95 for reported in issue.getElementsByTagName('acknowledgements'): # ASF httpd style credits
96 credit.append( { "lang":"eng", "value":re.sub('[\n ]+',' ', reported.childNodes[0].nodeValue.strip())} )
100 # Create the list of references
103 for adv in issue.getElementsByTagName('advisory'):
104 url = adv.getAttribute("url")
105 if (not url.startswith("htt")):
106 url = cfg.config['default_reference_prefix']+url
107 refs.append({"url":url,"name":url,"refsource":"CONFIRM"})
108 for git in issue.getElementsByTagName('git'): # openssl style references to git
109 url = cfg.config['git_prefix']+git.getAttribute("hash")
110 refs.append({"url":url,"name":url,"refsource":"CONFIRM"})
111 if cfg.config['project'] == 'httpd': # ASF httpd has no references so fake them
112 for fixed in issue.getElementsByTagName('fixed'):
113 base = "".join(fixed.getAttribute("version").split('.')[:-1])
114 refurl = cfg.config['default_reference']+base+".html#CVE-"+cvename
115 refs.append({"url":refurl,"name":refurl,"refsource":"CONFIRM"})
117 cve['references'] = { "reference_data": refs }
119 # Create the "affected products" list
122 for affects in issue.getElementsByTagName('fixed'): # OpenSSL and httpd since April 2018 does it this way
123 text = "Fixed in %s %s (Affected %s)" %(cfg.config['product_name'],affects.getAttribute('version'),cfg.merge_affects(issue,affects.getAttribute("base")))
124 # Let's condense into a list form since the format of this field is 'free text' at the moment, not machine readable (as per mail with George Theall)
125 vv.append({"version_value":text})
126 # Mitre want the fixed/affected versions in the text too
129 # if issue.getAttribute('fixed'): # httpd used to do it this way
130 # base = ".".join(issue.getAttribute("fixed").split('.')[:-1])+"."
131 # text = "Fixed in %s %s (Affected %s)" %(cfg.config['product_name'],issue.getAttribute('fixed'),cfg.merge_affects(issue,base))
132 # vv.append({"version_value":text})
133 # # Mitre want the fixed/affected versions in the text too
134 # desc += " "+text+"."
136 cve['affects'] = { "vendor" : { "vendor_data" : [ { "vendor_name": cfg.config['vendor_name'], "product": { "product_data" : [ { "product_name": cfg.config['product_name'], "version": { "version_data" : vv}}]}}]}}
138 # Mitre want newlines and excess spaces stripped
139 desc = re.sub('[\n ]+',' ', desc)
140 cve['description'] = { "description_data": [ { "lang":"eng", "value": desc} ] }
144 fn = issue['CVE_data_meta']['ID'] + ".json"
148 f = codecs.open(options.outputdir+"/"+fn, 'w', 'utf-8')
149 f.write(json.dumps(issue, sort_keys=True, indent=4))
150 print "wrote %s" %(options.outputdir+"/"+fn)
154 validate(issue, schema_doc)
155 print "%s passed validation" % (fn)
156 except jsonschema.exceptions.ValidationError as incorrect:
157 v = Draft4Validator(schema_doc)
158 errors = sorted(v.iter_errors(issue), key=lambda e: e.path)
160 print "%s did not pass validation: %s" % (fn,str(error.message))
162 print "%s skipping validation, no schema defined" %(fn)